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
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__esmMininit function is async only because of a transitive TLA dependency, the barrel's init still calls it withoutawait.Reproduction
REPL
What rolldown rc.15 emits
Because
init_barreldoes notawait init_middle(), the Promise frominit_middleis discarded. When the dynamic import ofbarrel.tsresolves,init_middlehas not finished, somanageris stillundefined.Difference from #8986
#8986 was the case where the module directly containing TLA is re-exported:
This issue is the transitive case:
middle.tshas no TLA of its own, but its__esmMininit becomesasyncbecause it mustawait 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.tshasawait enforceLicense()(TLA)gateway/routes/index.tstransitively importsoem/via config/middleware, makinginit_routesasyncgateway/index.tsis a pure barrel (export * from './routes/index.ts')init_gatewaycallsinit_routes()withoutawaitauthManager(exported from routes) isundefinedat runtime despiteawait import('gateway/index.ts')System Info