Fix `export * as X from './self'` in scope-hoisted modules (#93192)
### What?
Scope-hoisted execution of a module that re-exports its own namespace
(`export * as X from './self'`) returned the wrong namespace for named
imports of the module.
Given:
```js
// data.js
import * as Self from './data'
export function foo() { return 'foo' }
export function bar() { return 'bar' }
export function fooViaSelf() { return Self.foo() } // Self.foo undefined
export * as Data from './data'
```
Any binding imported from `./data` that relied on the module's own
namespace (e.g. `Self.foo`, `Data.foo`, `Data.Data.foo`) was `undefined`
at runtime. Without scope hoisting the same code worked correctly.
This PR fixes the bug and adds execution tests covering self-namespace
re-exports — with and without scope hoisting — including chained
re-exports (`Data.Data.foo`, `Data.Data.Data.bar`).
### Why?
For an access like `Self.Data` where `Self = import * as Self from
'./data'`
and `Data` is exposed through `export * as Data from './data'`, the
namespace-member-access optimization rewrites the reference to a named
import resolving to a synthesized rename module
(`./data <export * as Data>`). `ReferencedAsset::get_ident_inner` then
recurses through the rename's `EsmExport::ImportedNamespace("Data")` and
returns a `namespace_ident` derived from the inner module's chunk-item
id — but `EsmAssetReference::code_generation` independently took
`id = referenced_asset.chunk_item_id` and emitted
```js
var <inner-data-ident> = __turbopack_context__.i("<rename-id>");
```
so the variable named like `ns(data.js)` actually held
`ns(rename) = { Data: ns(data.js) }`. The non-optimized
`import * as Self` uses the same mangled name and sees the rename's
namespace, so `Self.foo()` evaluates to `undefined`.
### How?
Keep the variable name and the `.i(...)` argument consistent by moving
the "what to import" decision onto the ident itself:
- `ReferencedAssetIdent::Module` gains an `import_source: ImportSource`
field that describes what to import to populate the namespace variable.
- `ImportSource` is an enum:
- `Module { asset }` — carries a reference to the final module in any
re-export chain, from which the chunk-item id is lazily computed.
- `External { request, ty }` — carries everything needed to emit
`__turbopack_external_import` / `__turbopack_external_require`.
- The `namespace_ident` is cached in `ReferencedAssetIdent::Module` at
resolution time (computed via `ImportSource::get_namespace_ident()`)
so downstream sync visitors can read it without re-entering the async
layer.
- `ReferencedAsset::get_ident` / `get_ident_inner` populate the field.
For in-group re-exports the inner module propagates up; for external
references the `External` variant is used.
- `EsmAssetReference::code_generation` destructures
`ReferencedAssetIdent::Module { namespace_ident, ctxt, import_source, ..
}`
and dispatches purely on `import_source`; it no longer reads
`referenced_asset` after the `get_ident` call. The hoisted-statement
dedup key still uses the directly-referenced asset's id, so two
references that happen to resolve to the same inner module via
different paths (e.g. direct vs. through a rename) still emit
separate `var` declarations for AST merging to rename.
- ESM-external gating (`__turbopack_external_import` vs.
`__turbopack_external_require`) stays where it was — the emit site
reads `self.import_externals` from the surrounding
`EsmAssetReference`, so `ImportSource::External` does not carry it.
No additional `MergeableModuleExposure` or `additional_ids` changes are
needed. The rename module is never referenced at runtime; no snapshot
files change.
### Tests
-
`turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/self-reexport-star/`
— scope-hoisted execution test covering self-namespace re-exports,
nested access (`Data.Data.foo`, `Data.Data.Data.bar`), chained
re-exports through another module, and namespace key enumeration.
-
`turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/self-reexport-star-no-hoisting/`
— same test cases, run with scope hoisting disabled, reusing the
fixtures from the sibling directory.
Verified by `cargo test --test execution` (213 passed) and
`cargo test --test snapshot` (89 passed) in `turbopack-tests`. No
snapshot files are modified.
Closes NEXT-
Fixes #
<!-- NEXT_JS_LLM_PR -->
---------
Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>