Skip to content

fix: skip init-call transfer for circular wrapper deps#10270

Closed
marvinhagemeister wants to merge 2 commits into
rolldown:mainfrom
marvinhagemeister:fix/esm-min-circular-dep
Closed

fix: skip init-call transfer for circular wrapper deps#10270
marvinhagemeister wants to merge 2 commits into
rolldown:mainfrom
marvinhagemeister:fix/esm-min-circular-dep

Conversation

@marvinhagemeister

Copy link
Copy Markdown

When a wrapped ESM module imports another wrapped module that appears later in the output (circular imports), the init-call transfer logic in ensure_lazy_module_initialization_order can move an init_*() call to an earlier unwrapped module's top-level position. The call then runs before the later wrapper's var init_* = __esmMin(...) is assigned, causing TypeError: init_* is not a function.

This was reported in #10265 with onDemandWrapping, but the underlying issue can trigger whenever unwrapped modules coexist with circular wrapped dependencies.

Fixes #10265

Transferring an init_*() call to an earlier unwrapped module makes it
run eagerly at top level. If the wrapper body calls init_*() for a
module declared later in the output, that variable is still undefined
(TypeError: init_* is not a function).

This occurs with circular imports (a.js <-> b.js): moving init_b() to
an earlier position runs it before var init_a is assigned.

Skip the transfer when the wrapped module depends on any WrapKind::Esm
module with higher exec order than the transfer target. The init call
then stays inside the importer lazy wrapper closure where it is safe.

Fixes rolldown#10265
@codspeed-hq

codspeed-hq Bot commented Jul 14, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 7 untouched benchmarks
⏩ 10 skipped benchmarks1


Comparing marvinhagemeister:fix/esm-min-circular-dep (5b1d10c) with main (654075e)

Open in CodSpeed

Footnotes

  1. 10 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@hyfdev hyfdev self-assigned this Jul 14, 2026
@hyfdev

hyfdev commented Jul 14, 2026

Copy link
Copy Markdown
Member

onDemandWrapping is under a big refactor #10104. I'll test your case under the new implementation. Make sure it's covered. Will ship the new implementation next week.

@marvinhagemeister

Copy link
Copy Markdown
Author

Sweet, that's perfect! FWIW: I was able to reproduce it even in a larger codebase without onDemandWrapping enabled but wasn't able to get a clean minimal reproduction out of that. Using onDemandWrapping: true was the best I could do.

@IWANABETHATGUY

Copy link
Copy Markdown
Member

Sweet, that's perfect! FWIW: I was able to reproduce it even in a larger codebase without onDemandWrapping enabled but wasn't able to get a clean minimal reproduction out of that. Using onDemandWrapping: true was the best I could do.

Then I guess you probably did not fix the original issues in the large codebase, but thanks for reporting the bug with onDemandWrapping.

graphite-app Bot pushed a commit that referenced this pull request Jul 17, 2026
## Summary

- Add the circular ESM initialization graph reported in #10265, whose legacy init-call transfer failure is addressed by #10270.
- Exercise both strict modes: `strictExecutionOrder: true` by itself (wrap-all), and strict execution order with `experimental.onDemandWrapping: true`.
- Execute the generated entry in both cells and assert that the circular `init_a` / `init_b` path completes without calling an uninitialized wrapper.

## What this proves

The failure addressed by #10270 comes from the legacy `ensure_lazy_module_initialization_order()` transfer moving an `init_*()` call before a later wrapper assignment. With #10104's new strict implementation, that function is still called but returns before running the transfer logic; the wrap-all or on-demand execution-order analysis owns scheduling instead.

The exact circular graph executes successfully in both cells, demonstrating that this failure mode is absent from both new strict paths. Current `main` at `03e1e3422` also passes both strict configurations through the old implementation, so this is path-specific regression coverage rather than a `main`-fails / parent-passes claim.

## Scope

This conclusion is limited to strict execution order, in wrap-all and on-demand modes. Default and non-strict output still use the legacy transfer logic and are outside this test.

Refs #10265 and #10270.

## Validation

- `CARGO_TARGET_DIR=/tmp/codex-rolldown-pr10104-target just t-run crates/rolldown/tests/rolldown/issues/10265/_config.json` (twice; snapshot-stable)
- Both strict cells also pass on `main` at `03e1e3422`, with different generated layouts
- `just lint-node`
- `just lint-repo`
- `git diff --check`
@hyfdev

hyfdev commented Jul 20, 2026

Copy link
Copy Markdown
Member

Thanks for your effort. Ive added a test case under the new impl #10104 to ensure it's covered. New impl would be shipped this Wendesaday's release.

@hyfdev hyfdev closed this Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: onDemandWrapping emits circular init_* calls before initialization

3 participants