Skip to content

Fix export * as X from './self' in scope-hoisted modules#93192

Merged
sokra merged 14 commits intocanaryfrom
sokra/reexport-self
Apr 28, 2026
Merged

Fix export * as X from './self' in scope-hoisted modules#93192
sokra merged 14 commits intocanaryfrom
sokra/reexport-self

Conversation

@sokra
Copy link
Copy Markdown
Member

@sokra sokra commented Apr 24, 2026

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:

// 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

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 #

@sokra sokra requested a review from mischnic April 24, 2026 13:27
Comment thread turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs Outdated
Comment thread turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs Outdated
@sokra sokra force-pushed the sokra/reexport-self branch from ffeef7d to 730de91 Compare April 27, 2026 06:56
@github-actions github-actions Bot added created-by: Turbopack team PRs by the Turbopack team. Turbopack Related to Turbopack with Next.js. labels Apr 27, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 27, 2026

Stats from current PR

✅ No significant changes detected

📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 812ms 811ms █▁███
Cold (Ready in log) 780ms 777ms █▁██▇
Cold (First Request) 1.242s 1.212s ▇▁█▇▄
Warm (Listen) 813ms 811ms █▁███
Warm (Ready in log) 777ms 776ms █▁██▇
Warm (First Request) 575ms 577ms ▇▁█▇▃
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 811ms 810ms ▃▃▁▆▃
Cold (Ready in log) 793ms 792ms █▄▇▅▅
Cold (First Request) 3.302s 3.300s █▇█▆█
Warm (Listen) 811ms 810ms ▃▆▆▆▁
Warm (Ready in log) 792ms 795ms █▃▆▄▆
Warm (First Request) 3.303s 3.325s ▄▅█▂▆

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 5.065s 4.982s ▆▁█▇▆
Cached Build 5.027s 4.949s █▁█▇▇
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 23.990s 23.795s █▁▂▄▄
Cached Build 24.118s 24.117s █▁▂▆▂
node_modules Size 495 MB 495 MB ████▃
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
0_09canb0ezn8.js gzip 153 B N/A -
02k5onwb4z3s6.js gzip 167 B N/A -
03t9nzq88k1pe.js gzip 155 B N/A -
04tqxk-qcsi2f.js gzip 156 B N/A -
0cz1d0mv5g_q7.js gzip 39.4 kB 39.4 kB
0fli3_wppnim5.js gzip 12.9 kB N/A -
0kb7_ep3r1z0_.js gzip 10.1 kB N/A -
0kw8xgqdrilf6.js gzip 8.56 kB N/A -
0nnx767ck3zz4.js gzip 157 B N/A -
0ojkk2e654xsc.js gzip 8.59 kB N/A -
0wxpyd8r-vipl.js gzip 1.47 kB N/A -
0xrh8l7b4d3s2.js gzip 156 B N/A -
0xy2fhla48_rd.js gzip 9.24 kB N/A -
10wqsvi2mgfmi.js gzip 9.82 kB N/A -
16lhqjoqbznyg.js gzip 220 B 220 B
16vepdkipri3r.js gzip 8.51 kB N/A -
17n96uu6y1pxq.js gzip 8.6 kB N/A -
18y4_8-9or0mn.js gzip 8.51 kB N/A -
1elt1qium-r2m.css gzip 115 B 115 B
1gq145j3kps-h.js gzip 8.62 kB N/A -
1k3mvlb4-0ngf.js gzip 155 B N/A -
1ke_4s9soy654.js gzip 156 B N/A -
1nsh-mbn0e-se.js gzip 8.56 kB N/A -
1qngoc418rk6i.js gzip 65.6 kB N/A -
1r3s1n8vyb7h6.js gzip 161 B N/A -
1tsrrp1tdngti.js gzip 13.3 kB N/A -
1v-qecyz63-0b.js gzip 154 B N/A -
2__-e_ym8n788.js gzip 450 B N/A -
22o6xd9_ywdu6.js gzip 233 B N/A -
25n272-g99oa1.js gzip 7.61 kB N/A -
2c9mvd-i9rxxl.js gzip 160 B N/A -
2d4njk_907vw4.js gzip 157 B N/A -
2faj3acmavn9n.js gzip 13.1 kB N/A -
2kvj8yrfznmwx.js gzip 5.69 kB N/A -
2qv7m7xjnokgr.js gzip 8.58 kB N/A -
2ue5g3yr_f1ds.js gzip 70.9 kB N/A -
342ijzvrpe53h.js gzip 2.29 kB N/A -
3afk9e9-iuwwd.js gzip 157 B N/A -
3k1k5gtofm6eq.js gzip 10.4 kB N/A -
3xq6of2nocani.js gzip 49.5 kB N/A -
42_02jza_7yny.js gzip 13.8 kB N/A -
turbopack-04..w00-.js gzip 4.19 kB N/A -
turbopack-0g..9a3o.js gzip 4.19 kB N/A -
turbopack-0l..g-ev.js gzip 4.19 kB N/A -
turbopack-0m..2-2t.js gzip 4.18 kB N/A -
turbopack-0t..xwt-.js gzip 4.19 kB N/A -
turbopack-0u..t26z.js gzip 4.2 kB N/A -
turbopack-1k..1uu6.js gzip 4.19 kB N/A -
turbopack-1m..q5n1.js gzip 4.19 kB N/A -
turbopack-2j..87q5.js gzip 4.19 kB N/A -
turbopack-2q..y41_.js gzip 4.19 kB N/A -
turbopack-2y..xjoo.js gzip 4.19 kB N/A -
turbopack-3l..z3of.js gzip 4.17 kB N/A -
turbopack-3s..s0yf.js gzip 4.19 kB N/A -
turbopack-3u..-zq7.js gzip 4.19 kB N/A -
03kysncgx5l7w.js gzip N/A 155 B -
0arkbdqpxc37i.js gzip N/A 8.6 kB -
0bz-xifewa17d.js gzip N/A 8.63 kB -
0efh6erg1kc4c.js gzip N/A 158 B -
0tvekitj587fh.js gzip N/A 8.51 kB -
0yvk6-wi8e9wh.js gzip N/A 13.3 kB -
1-jqyfc89tixo.js gzip N/A 1.46 kB -
10y3h86mnhs_2.js gzip N/A 10.4 kB -
12hxdatac0fxj.js gzip N/A 49.5 kB -
139jydanoq6-d.js gzip N/A 154 B -
14t1kneseb8th.js gzip N/A 2.3 kB -
15sb1-dsqfk_j.js gzip N/A 8.59 kB -
1ab2xruymo-oj.js gzip N/A 449 B -
1b3xo3p2pa8_a.js gzip N/A 70.9 kB -
1dt49_v4y8lxb.js gzip N/A 13.8 kB -
1tu25qtsmfhar.js gzip N/A 9.82 kB -
1v3ftpmn8m_ud.js gzip N/A 161 B -
1vein_gnv3mwr.js gzip N/A 8.56 kB -
1vmibvuhp1gey.js gzip N/A 13.1 kB -
1wzrm0xjjbzn5.js gzip N/A 10.1 kB -
1z1geo4e53wn1.js gzip N/A 156 B -
1z3g0uaqtv9_3.js gzip N/A 8.56 kB -
2-2ld71a0y6d5.js gzip N/A 157 B -
213wdc0nef-no.js gzip N/A 168 B -
248gz0gduuney.js gzip N/A 157 B -
2bi5hx402juv-.js gzip N/A 8.58 kB -
2hy56297fog9u.js gzip N/A 8.52 kB -
2k0exemzm1ral.js gzip N/A 157 B -
2pch5duiz7pl1.js gzip N/A 155 B -
2u_rpxq3tzytl.js gzip N/A 233 B -
2zg0rr542d7qb.js gzip N/A 156 B -
314cbinszt68n.js gzip N/A 155 B -
35nh2lh_i5pyh.js gzip N/A 7.61 kB -
368lim5wq0o0r.js gzip N/A 12.9 kB -
3drqjohogojbw.js gzip N/A 5.69 kB -
3inn3g12k7ggr.js gzip N/A 153 B -
3lx6lyx6jwnsa.js gzip N/A 65.5 kB -
3wpp8nvyoj121.js gzip N/A 9.24 kB -
turbopack-02..h8bk.js gzip N/A 4.19 kB -
turbopack-03..d822.js gzip N/A 4.19 kB -
turbopack-04..-sj1.js gzip N/A 4.19 kB -
turbopack-0d..etty.js gzip N/A 4.19 kB -
turbopack-0e..h9db.js gzip N/A 4.19 kB -
turbopack-0h..um4t.js gzip N/A 4.18 kB -
turbopack-0m.._t4k.js gzip N/A 4.19 kB -
turbopack-0o..jeg1.js gzip N/A 4.19 kB -
turbopack-15..3bjj.js gzip N/A 4.2 kB -
turbopack-18..r3ht.js gzip N/A 4.19 kB -
turbopack-1b..dtt2.js gzip N/A 4.19 kB -
turbopack-3b..mz6c.js gzip N/A 4.19 kB -
turbopack-3n..av5a.js gzip N/A 4.17 kB -
turbopack-3y..9apy.js gzip N/A 4.19 kB -
Total 465 kB 465 kB ⚠️ +48 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 718 B 719 B
Total 718 B 719 B ⚠️ +1 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 435 B 433 B
Total 435 B 433 B ✅ -2 B

📦 Webpack

Client

Main Bundles
Canary PR Change
2637-HASH.js gzip 4.63 kB N/A -
7724.HASH.js gzip 169 B N/A -
8274-HASH.js gzip 61.4 kB N/A -
8817-HASH.js gzip 5.59 kB N/A -
c3500254-HASH.js gzip 62.8 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 254 B 255 B
main-HASH.js gzip 39.4 kB 39.4 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
5887-HASH.js gzip N/A 5.61 kB -
6522-HASH.js gzip N/A 60.7 kB -
6779-HASH.js gzip N/A 4.63 kB -
8854.HASH.js gzip N/A 169 B -
eab920f9-HASH.js gzip N/A 62.8 kB -
Total 236 kB 235 kB ✅ -652 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 193 B 193 B
_error-HASH.js gzip 182 B 182 B
css-HASH.js gzip 333 B 334 B
dynamic-HASH.js gzip 1.81 kB 1.8 kB
edge-ssr-HASH.js gzip 255 B 255 B
head-HASH.js gzip 353 B 349 B 🟢 4 B (-1%)
hooks-HASH.js gzip 384 B 382 B
image-HASH.js gzip 581 B 581 B
index-HASH.js gzip 260 B 259 B
link-HASH.js gzip 2.52 kB 2.52 kB
routerDirect..HASH.js gzip 316 B 318 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 313 B 314 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.99 kB 7.98 kB ✅ -10 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 126 kB 126 kB
page.js gzip 274 kB 274 kB
Total 400 kB 399 kB ✅ -518 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 616 B 614 B
middleware-r..fest.js gzip 156 B 156 B
middleware.js gzip 44 kB 44.4 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 45.6 kB 46 kB ⚠️ +390 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 722 B 719 B
Total 722 B 719 B ✅ -3 B
Build Cache
Canary PR Change
0.pack gzip 4.4 MB 4.4 MB 🟢 4.47 kB (0%)
index.pack gzip 114 kB 114 kB
index.pack.old gzip 113 kB 115 kB 🔴 +2.06 kB (+2%)
Total 4.63 MB 4.63 MB ✅ -2.29 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 348 kB 348 kB
app-page-exp..prod.js gzip 193 kB 193 kB
app-page-tur...dev.js gzip 348 kB 348 kB
app-page-tur..prod.js gzip 193 kB 193 kB
app-page-tur...dev.js gzip 344 kB 344 kB
app-page-tur..prod.js gzip 191 kB 191 kB
app-page.run...dev.js gzip 345 kB 345 kB
app-page.run..prod.js gzip 191 kB 191 kB
app-route-ex...dev.js gzip 77.3 kB 77.3 kB
app-route-ex..prod.js gzip 52.8 kB 52.8 kB
app-route-tu...dev.js gzip 77.4 kB 77.4 kB
app-route-tu..prod.js gzip 52.8 kB 52.8 kB
app-route-tu...dev.js gzip 77 kB 77 kB
app-route-tu..prod.js gzip 52.5 kB 52.5 kB
app-route.ru...dev.js gzip 76.9 kB 76.9 kB
app-route.ru..prod.js gzip 52.5 kB 52.5 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 44.2 kB 44.2 kB
pages-api-tu..prod.js gzip 33.7 kB 33.7 kB
pages-api.ru...dev.js gzip 44.2 kB 44.2 kB
pages-api.ru..prod.js gzip 33.7 kB 33.7 kB
pages-turbo....dev.js gzip 53.7 kB 53.7 kB
pages-turbo...prod.js gzip 39.4 kB 39.4 kB
pages.runtim...dev.js gzip 53.6 kB 53.6 kB
pages.runtim..prod.js gzip 39.4 kB 39.4 kB
server.runti..prod.js gzip 63.1 kB 63.1 kB
Total 3.08 MB 3.08 MB
📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/4692cbdccd06f692a3cd334ef8d97ed0e46e5c0c/next

Commit: 4692cbd

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 27, 2026

Tests Passed

Commit: 4692cbd

@mischnic mischnic marked this pull request as ready for review April 27, 2026 16:15
Comment thread turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs
sokra and others added 14 commits April 28, 2026 21:21
Expands the existing `self-reexport-star` test and adds a
`self-reexport-star-no-hoisting` variant (scopeHoisting disabled) that
covers the same scenarios. Tests verify that the self re-exported
namespace is the same object as the module's own namespace, supports
recursive `Data.Data.Data` access, can be re-exported through a chained
module, and enumerates all expected keys.

Co-Authored-By: Claude <noreply@anthropic.com>
Replace copied data.js/reexport.js/index.js in the no-hoisting variant
with a single index.js that imports the sibling self-reexport-star
entry point. Also expand both tests with the failing `import * as Self`
scenario and additional assertions.

Co-Authored-By: Claude <noreply@anthropic.com>
`export * as X from './self'` compiled as an in-group namespace
re-export produced a `var __TURBOPACK__imported__module__<esm> =
__turbopack_context__.i("<esm> <export * as X>")`. That declaration
aliased the synthesized rename module's id to the outer module's
namespace variable, but the rename module's factory was not registered
when its exposure was `MergeableModuleExposure::None`. The runtime
lookup failed with "module factory is not available".

Two changes:

- In `ReferencedAsset::get_ident_inner`, stop recursing into
  `EsmExport::ImportedNamespace` when the target is in the same scope
  hoisting group. The outer asset is the correct module to import; the
  inner asset can be a different module (e.g. a rename module) whose
  chunk-item id doesn't match the emitted `namespace_ident`.
- Expose intra-group referenced modules as at least `Internal` and
  register their factories in `additional_ids`. Intra-group references
  via `__turbopack_context__.i(id)` require the runtime lookup to find
  the target id.

Co-Authored-By: Claude <noreply@anthropic.com>
Reworks the previous fix which over-exposed modules. Instead of
promoting every intra-group referenced module to
`MergeableModuleExposure::Internal` and including all `Internal`
modules in `additional_ids` (which updated four unrelated snapshots),
only modules that opt in via a new `MergeableModule::requires_namespace_exposure`
hook are promoted — and they are promoted all the way to `External` so
they appear in `additional_ids` without expanding the set of modules
exposed through that mechanism.

- Add `MergeableModule::requires_namespace_exposure` (default `false`).
- Implement it returning `true` for `EcmascriptModuleRenameModule`,
  since its emitted code always contains a
  `__turbopack_context__.i(<self id>)` lookup.
- In `compute_merged_modules`, promote intra-group referenced modules
  that opt in to `External` exposure (wrapped in an `async {}.instrument(...)`
  block to keep the future `Send` for `turbo_tasks::function`).
- Revert the `additional_ids` widening in `lib.rs` that included
  `Internal` modules.
- Keep the minimal `ReferencedAsset::get_ident_inner` change for
  `EsmExport::ImportedNamespace` so the `namespace_ident` matches the
  outer asset's chunk item id used by callers when emitting
  `var <ident> = __turbopack_context__.i(id)`.

Restores the four snapshot files touched by the previous fix to their
canary state. No other snapshots change. 213 execution and 89 snapshot
tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>
… and "Fix intra-group namespace re-exports in scope hoisting"

Both fixes addressed a symptom (module factory lookup failure for
`export * as X from './self'`) instead of the root cause. Investigation
showed the actual issue is in `ReferencedAsset::get_ident_inner` /
`EsmAssetReference::code_generation`: the emitted
`var <namespace_ident> = __turbopack_context__.i(<id>)` uses the inner
module's ident as the variable name but the immediately-referenced
(rename) module's id for the runtime lookup, causing the variable to
hold the rename module's namespace instead of the inner module's. The
previous fixes papered over this by exposing the rename module's
factory; this revert restores the branch to a clean state so the real
fix can be made narrowly in `get_ident_inner` / `code_generation`.

Keeps the test coverage added in 2000ea7 and 64922a3. The self-reexport
tests still fail at this point; fix will follow in a separate commit.

Co-Authored-By: Claude <noreply@anthropic.com>
For `Self.Data` where `Self = import * as Self from './inner'` and `Data`
is itself exposed through `export * as Data from './inner'`, the
namespace-member-access optimization rewrites the reference to a named
import resolving to a rename module (`./inner <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 — while `code_generation`
initializes that same variable with `__turbopack_context__.i(<id>)`
using the directly-referenced (rename) module's id. The variable ends
up holding `ns(rename) = { Data: ns(inner) }` instead of `ns(inner)`,
and because the non-optimized `import * as Self` uses the same mangled
name, it sees the wrong namespace and `Self.foo()` is `undefined`.

Track the target module alongside the namespace ident in
`ReferencedAssetIdent::Module::import_module` and use its chunk-item id
for the `.i(...)` call, so the variable and its initializer refer to
the same module.

No additional module exposure is needed: the rename module is never
referenced at runtime, and no snapshots change.

Co-Authored-By: Claude <noreply@anthropic.com>
In the `ReferencedAsset::Some` arm, use `import_module` from the
`ReferencedAssetIdent::Module` we're emitting, instead of pulling the
chunk item id from `referenced_asset` and then conditionally overriding
it. Keep the directly-referenced `asset` only for the hoisted-statement
dedup key — two refs to the same `import_module` via different paths
(direct vs. re-export rename) may arrive with different syntax contexts
and must emit separate `var` declarations for AST merging to rename.

Co-Authored-By: Claude <noreply@anthropic.com>
Previously `ReferencedAssetIdent::Module::import_module` was
`Option<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>`, and the
emitting `EsmAssetReference::code_generation` then re-inspected
`referenced_asset` to decide between module / external-esm /
external-commonjs paths. That made the emission dependent on the
directly-referenced asset, not the ident — which was exactly what the
self-reexport bug required we stop doing.

Replace the field with an `ImportSource` enum whose `Module(ModuleId)`
variant carries the chunk-item id directly, and whose `External {
request, ty, import_externals }` variant carries everything needed to
emit `__turbopack_external_import` / `__turbopack_external_require`
without re-consulting the original `ReferencedAsset`. Plumb
`import_externals` through `ReferencedAsset::get_ident`; non-emitting
callers pass `false` with a comment explaining the field is unread.

In the emit block, destructure the ident directly in the outer pattern
and dispatch on `ImportSource` — `referenced_asset` is no longer used
past `get_ident`.

Co-Authored-By: Claude <noreply@anthropic.com>
The emit site for `ReferencedAssetIdent::Module` in
`EsmAssetReference::code_generation` already reads
`this.import_externals` from the surrounding reference. Carrying it on
the ident duplicated that information and forced every non-emitting
`get_ident` caller to pass an unused `bool`. Remove the field and drop
the `import_externals` parameter from `ReferencedAsset::get_ident` /
`get_ident_inner`; the ESM-external branch in `code_generation` now
reads the local `import_externals` binding directly.

Co-Authored-By: Claude <noreply@anthropic.com>
Store the imported module's `asset` on `ImportSource::Module` (instead
of its `ModuleId`) and the namespace variable name on each variant,
exposed via `ImportSource::get_namespace_ident()`. The field on
`ReferencedAssetIdent::Module` is renamed `import_module` -> `import_source`.
The chunk item id is computed on demand at the emit site.
Drop the stored `namespace_ident` from `ImportSource` variants and
compute it on demand in `ImportSource::get_namespace_ident`, which now
takes a `ChunkingContext` (needed to resolve the module id for
`ImportSource::Module`). `ReferencedAssetIdent::Module` keeps a cached
`namespace_ident` populated at resolution time so sync visitor closures
don't need async access.
Instead of storing a generic Module and upcasting, store the
EcmascriptChunkPlaceable directly. This simplifies the code and allows
direct access to chunk_item_id without needing a separate hoist_key
variable.
Prevents incorrect deduplication when multiple merged modules import the
same target but have different syntax contexts, which would cause
hygiene to rename one of them.
@sokra sokra force-pushed the sokra/reexport-self branch from c895f99 to 4692cbd Compare April 28, 2026 19:21
@sokra sokra merged commit dab281a into canary Apr 28, 2026
184 checks passed
@sokra sokra deleted the sokra/reexport-self branch April 28, 2026 19:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

created-by: Turbopack team PRs by the Turbopack team. Turbopack Related to Turbopack with Next.js.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants