Skip to content

[Bug]: barrel init missing await on async dependency makes export wildcard + transitive top-level await broken #9083

@thezzisu

Description

@thezzisu

Description

Issue #8986 was closed as fixed in rc.13 and the fix addressed the direct case where a module with TLA is directly re-exported via export * from. However, the transitive case is still broken in rc.15: when a barrel re-exports a module whose __esmMin init function is async only because of a transitive TLA dependency, the barrel's init still calls it without await.

Reproduction

// deep.ts — has top-level await
export const value = 'hello'
await Promise.resolve()  // TLA here

// middle.ts — transitively depends on deep.ts (no own TLA)
import { value } from './deep.ts'
export const manager = { value, ready: false }
export function setup() { manager.ready = true }

// barrel.ts — pure barrel re-export
export * from './middle.ts'

// main.ts
async function run() {
  const { manager, setup } = await import('./barrel.ts')
  setup()
  console.log(manager.ready) // expected: true, actual: crashes (manager is undefined)
}
run()

REPL

What rolldown rc.15 emits

var init_middle = __esmMin((async () => {
  await init_deep()        // ✅ correct — middle depends on deep (async)
  manager = { ... }
}))

var init_barrel = __esmMin((async () => {
  init_middle()            // ❌ missing await! init_middle is async
}))

Because init_barrel does not await init_middle(), the Promise from init_middle is discarded. When the dynamic import of barrel.ts resolves, init_middle has not finished, so manager is still undefined.

Difference from #8986

#8986 was the case where the module directly containing TLA is re-exported:

barrel.ts  --export * from-->  deep.ts (has TLA)

This issue is the transitive case:

barrel.ts  --export * from-->  middle.ts (no TLA)  --import-->  deep.ts (has TLA)

middle.ts has no TLA of its own, but its __esmMin init becomes async because it must await init_deep(). The barrel's init function still does not await this async dependency.

Real-world impact

Encountered in secflow, a Node.js SEA bundle:

  • oem/index.ts has await enforceLicense() (TLA)
  • gateway/routes/index.ts transitively imports oem/ via config/middleware, making init_routes async
  • gateway/index.ts is a pure barrel (export * from './routes/index.ts')
  • Bundle output: init_gateway calls init_routes() without await
  • authManager (exported from routes) is undefined at runtime despite await import('gateway/index.ts')

System Info

rolldown: 1.0.0-rc.15 (pinned via yarn resolutions)
tsdown: 0.21.7
Node.js: v25.9.0

Metadata

Metadata

Assignees

Type

Priority

None yet

Effort

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions