Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use crate::{
module_fragments::part::module::EcmascriptModulePartAsset,
references::esm::base::ReferencedAsset,
runtime_functions::{TURBOPACK_DYNAMIC, TURBOPACK_ESM},
side_effect_optimization::locals::module::EcmascriptModuleLocalsModule,
utils::module_id_to_lit,
};

Expand Down Expand Up @@ -159,6 +160,19 @@ pub async fn follow_reexports(
let mut module = module;
let mut export_name = export_name;
loop {
// A locals module only exposes local bindings, so there are no more reexports to follow.
// Returning it still preserves its evaluation and side effects. Avoid asking it for side
// effects or exports from the original module: resolving the facade's synthetic locals
// reference can happen while that module is still being analyzed, and either request would
// create a dependency cycle.
if ResolvedVc::try_downcast_type::<EcmascriptModuleLocalsModule>(module).is_some() {
return Ok(FollowExportsResult::cell(FollowExportsResult {
module,
export_name: Some(export_name),
ty: FoundExportType::Found,
}));
}

if !ignore_side_effects
&& *module.side_effects().await? != ModuleSideEffects::SideEffectFree
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { B } from './B'

// This re-export is what forces the facade/locals split for this module
// (`EcmascriptExports::split_locals_and_reexports` returns true as soon as a
// module has any `ImportedBinding`/star re-export). No other option is needed.
export { helper } from './helper'

export function A(n: number) {
if (n > 0) {
B(n - 1)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { C } from './C'
import { asyncFn } from './asyncFn'

export { helper } from './helper'

export function B(n: number) {
if (n > 0) {
C(n - 1)
asyncFn()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { A } from './A'

export { helper } from './helper'

export function C(n: number) {
if (n > 0) {
A(n - 1)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
// Top-level await makes this an async module, which is what drags the whole
// import cycle into async-module handling.
await sleep(0)

export function asyncFn() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function helper() {
return 'helper'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { A } from './A'

/*
* Regression test: turbopack used to hang (deadlock) while building the module
* graph for an import cycle whose modules are split into facade + locals
* modules.
*
* Topology (same shape as ../../async-modules/cycle-2, plus re-exports):
*
* index -> A
* ^\
* | v
* C<-B -> asyncFn (top-level await)
*
* A, B and C each carry `export { helper } from './helper'`. That re-export is
* enough to make `EcmascriptExports::split_locals_and_reexports` return true,
* so each of them is split into an `EcmascriptModuleFacadeModule` plus an
* `EcmascriptModuleLocalsModule`.
*
* Resolving `import { A } from './A'` goes through `apply_reexport_tree_shaking`
* (module resolution, `turbopack/src/lib.rs`), which calls
* `follow_reexports(A_facade, "A")`. That walks facade -> locals, and the locals
* step used to ask the locals module for its side effects, which are derived
* from the original module's `analyze()` — already in flight further up the same
* import cycle. The result was a turbo-tasks await cycle: the process sat at
* ~0.5% CPU with completely flat RSS and never finished, so `next build` would
* hang with no output and no error.
*
* The async module is not required to trigger this; see
* `../reexport-cycle-deadlock` for the same cycle without a top-level `await`.
*/

it('should not deadlock building a re-exporting import cycle with an async module', () => {
A(10)
expect(true).toBe(true)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { B } from './B'

// This re-export is what forces the facade/locals split for this module
// (`EcmascriptExports::split_locals_and_reexports` returns true as soon as a
// module has any `ImportedBinding`/star re-export). No other option is needed.
export { helper } from './helper'

export function A(n: number) {
if (n > 0) {
B(n - 1)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { C } from './C'
import { syncFn } from './syncFn'

export { helper } from './helper'

export function B(n: number) {
if (n > 0) {
C(n - 1)
syncFn()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { A } from './A'

export { helper } from './helper'

export function C(n: number) {
if (n > 0) {
A(n - 1)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function helper() {
return 'helper'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { A } from './A'

/*
* Regression test: turbopack used to hang (deadlock) while building the module
* graph for an import cycle whose modules are split into facade + locals
* modules.
*
* Topology:
*
* index -> A
* ^\
* | v
* C<-B -> syncFn
*
* A, B and C each carry `export { helper } from './helper'`. That re-export is
* enough to make `EcmascriptExports::split_locals_and_reexports` return true,
* so each of them is split into an `EcmascriptModuleFacadeModule` plus an
* `EcmascriptModuleLocalsModule`.
*
* This is the same bug as `../reexport-cycle-async-deadlock`, but without any
* async module: a top-level `await` anywhere in the cycle is not required to
* trigger it. The re-export cycle alone is enough.
*/

it('should not deadlock building a re-exporting import cycle', () => {
A(10)
expect(true).toBe(true)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export function syncFn() {}
Loading