…deadlock
Turbopack deadlocks while building the module graph for an import cycle whose
modules are split into facade + locals modules and which contains an async
module. This reproduces on canary with no configuration: a single
`export { x } from './y'` in each cycle member is enough to trigger the split
(`EcmascriptExports::split_locals_and_reexports`), and one top-level `await`
makes the cycle async.
Symptom: the build never finishes. The process sits at ~0.5% CPU with entirely
flat RSS and never spawns a Node process, so it is an await that never resolves
on the Rust side, before any generated code runs -- not a spin loop and not
unbounded graph growth. For a real project this is `next build` hanging with no
output and no error; in CI it shows up only as a job timeout.
Where it stalls, from instrumented tracing:
1. The module-graph builder blocks in `primary_chunkable_referenced_modules` on
`module.references().await` for the entry module, i.e. inside `analyze()`.
2. Resolving `import { A } from './A'` reaches `apply_reexport_tree_shaking`
(`turbopack/src/lib.rs`), which calls `follow_reexports(A_facade, "A")`.
3. `follow_reexports` takes exactly two steps -- facade, then `<locals>` -- and
never completes that iteration.
4. The stall is inside the locals module's `get_exports()`, which awaits the
original module's `analyze()`, already in flight further up the same import
cycle. That closes a turbo-tasks await cycle.
`followReexports: false` makes it pass immediately, which pins the stall to the
`follow_reexports` path rather than to async-module handling generally. Note the
topology is deliberately close to `execution/turbopack/async-modules/cycle-2`,
whose header documents an earlier bug in `compute_async_module_info_single` for
the same shape; this is a different failure and that function is never reached.
The fixture is parked under `__hangs__/`, a path that matches neither fixture
glob in `execution.rs`, so nothing runs it automatically. `__skipped__` is not
usable for a hang: `test_skipped_fails` still executes its fixtures (under
`#[should_panic]`), so a fixture that never returns would stall the whole
`test cargo unit` job. The README explains how to arm it by moving it up one
directory; both the inert state (`running 0 tests`) and the armed state
(exit 124) were verified.
Found while enabling export mangling by default in this suite
(#97672 / #97676 / #97770). Mangling makes this far easier to hit, because it
splits every module with exports rather than only re-exporting ones, but the bug
is independent of mangling and predates that work -- hence this reproduction on
plain canary.
<!-- NEXT_JS_LLM -->
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
What?
Prevents Turbopack from deadlocking while following re-exports through a facade/locals-split module in an import cycle.
The supplied async-cycle regression is promoted into the normal execution suite. A synchronous variant is included as well because the investigation established that top-level
awaitis not required: the split re-export cycle alone can trigger the deadlock.Why?
apply_reexport_tree_shakingfollows re-exports during module resolution, which itself runs inside module analysis. After following a local export from a facade to its locals module, the previous implementation queried that locals module's side effects. In the absence of a package-level side-effects declaration, that query awaited the original module's analysis.For a cycle such as A → B → C → A, this closed an analysis dependency cycle and left the build waiting indefinitely before generated code could run. This also blocks the export-mangling work in #97770, which makes facade/locals splitting more common.
How?
A locals module is now treated as terminal by
follow_reexports. It exposes only local bindings, so there is no further re-export to follow; returning it directly preserves its evaluation and side effects while avoiding an analysis-dependent side-effect/export query during resolution.This keeps facade/locals splitting and re-export tree shaking enabled rather than narrowing either optimization.
Verification
cargo test -p turbopack-tests --test execution— 262 passed, 0 failedcargo fmt -- --check