Repository: rolldown/rolldown
Pull request #4626: fix: `preserveModules` co exists with multi entrypoints
Author: login: IWANABETHATGUY
Labels: name: needs-triage
Created: 2025-05-21T08:09:51Z | Merged: 2025-05-21T09:31:28Z

<!-- Thank you for contributing! -->

### Description
1. Closed #4624
<!-- Please insert your description here and provide especially info about the "what" this PR is solving -->


Linked issue #4624: [Bug]: Generated imports reference non-existent exports when using preserveModules/unbundle
### Reproduction link or steps

https://github.com/pcfreak30/rolldown-unbundle-repro

### What is expected?

```
// dist/esm/a.js
import { b_default } from "./b.js"; // ❌ Importing non-existent named export
export { b_default as B };

// dist/esm/b.js
export { b_default as default }; // ✅ Only exports default
```

### What is actually happening?

```
// Invalid import of non-existent named export
import { b_default } from "./b.js";
export { b_default as B };
```

### System Info

```Shell
System:
    OS: Linux 5.15 Linux Mint 21.3 (Virginia)
    CPU: (16) x64 AMD Ryzen 7 1700 Eight-Core Processor
    Memory: 7.66 GB / 62.72 GB
    Container: Yes
    Shell: 5.1.16 - /bin/bash
  Binaries:
    Node: 22.14.0 - ~/.nvm/versions/node/v22.14.0/bin/node
    Yarn: 3.3.0 - ~/.nvm/versions/node/v22.14.0/bin/yarn
    npm: 10.9.2 - ~/.nvm/versions/node/v22.14.0/bin/npm
    bun: 1.2.6 - ~/.bun/bin/bun
  Browsers:
    Brave Browser: 135.1.77.101
    Chrome: 135.0.7049.114
```

### Any additional comments?

_No response_
Linked issue comments: user: login: IWANABETHATGUY
created_at: 2025-05-21T01:30:34Z
body: Can't reproduce your issues, please provide a valid reproduction
author_association: MEMBER
user: login: pcfreak30
created_at: 2025-05-21T01:52:11Z
body: > Can't reproduce your issues, please provide a valid reproduction

I just pulled this and ran in a cleanroom `node:22` docker image, ran `./node_modules/.bin/tsdown` and reproduced the issue I am seeing. 
author_association: NONE
user: login: IWANABETHATGUY
created_at: 2025-05-21T01:57:43Z
body: You should not set `entry: ['src/**/*']` when you enable unbundle， they are functionally equivalent.
author_association: MEMBER
user: login: pcfreak30
created_at: 2025-05-21T02:08:29Z
body: > You should not set `entry: ['src/**/*']` when you enable unbundle， they are functionally equivalent.

Ok, what is entry intended to be? And it still seems wrong that a defaults vs named export ever gets a mix-up here?
author_association: NONE
user: login: IWANABETHATGUY
created_at: 2025-05-21T02:14:13Z
body: > > You should not set `entry: ['src/**/*']` when you enable unbundle， they are functionally equivalent.
> 
> Ok, what is entry intended to be? And it still seems wrong that a defaults vs named export ever gets a mix-up here?

I think for your case, it should be `entry: 'src/index.ts'`
author_association: MEMBER
user: login: pcfreak30
created_at: 2025-05-21T02:15:40Z
body: > > > You should not set `entry: ['src/**/*']` when you enable unbundle， they are functionally equivalent.
> > 
> > 
> > Ok, what is entry intended to be? And it still seems wrong that a defaults vs named export ever gets a mix-up here?
> 
> I think for your case, it should be `entry: 'src/index.ts'`

Also while it might be moot it behaves the same (bugged) if passed the dir `entry: ["src"],`.
author_association: NONE
user: login: IWANABETHATGUY
created_at: 2025-05-21T02:21:10Z
body: > > > > You should not set `entry: ['src/**/*']` when you enable unbundle， they are functionally equivalent.
> > > 
> > > 
> > > Ok, what is entry intended to be? And it still seems wrong that a defaults vs named export ever gets a mix-up here?
> > 
> > 
> > I think for your case, it should be `entry: 'src/index.ts'`
> 
> Also while it might be moot it behaves the same (bugged) if passed the dir `entry: ["src"],`.

I guess it related to tsdown, maybe it did not resolve `src` to `src/index.ts`, instead it treat it as a glob pattern `src/**` 
author_association: MEMBER
user: login: pcfreak30
created_at: 2025-05-21T02:29:41Z
body: > > > > > You should not set `entry: ['src/**/*']` when you enable unbundle， they are functionally equivalent.
> > > > 
> > > > 
> > > > Ok, what is entry intended to be? And it still seems wrong that a defaults vs named export ever gets a mix-up here?
> > > 
> > > 
> > > I think for your case, it should be `entry: 'src/index.ts'`
> > 
> > 
> > Also while it might be moot it behaves the same (bugged) if passed the dir `entry: ["src"],`.
> 
> I guess it related to tsdown, maybe it did not resolve `src` to `src/index.ts`, instead it treat it as a glob pattern `src/**`

Not sure. I view tsdown as a very light config wrapper for DX thus I opened the issue here vs there.
author_association: NONE

Diff:
diff --git a/crates/rolldown/src/ecmascript/format/cjs.rs b/crates/rolldown/src/ecmascript/format/cjs.rs
index f5041cff6e46..4202d98ff448 100644
--- a/crates/rolldown/src/ecmascript/format/cjs.rs
+++ b/crates/rolldown/src/ecmascript/format/cjs.rs
@@ -1,7 +1,7 @@
 use crate::utils::chunk::determine_export_mode::determine_export_mode;
 use crate::utils::chunk::namespace_marker::render_namespace_markers;
 use crate::utils::chunk::render_chunk_exports::{
-  get_chunk_export_names, render_wrapped_entry_chunk,
+  get_chunk_export_names_with_ctx, render_wrapped_entry_chunk,
 };
 use crate::{
   ecmascript::ecma_generator::RenderedModuleSources, types::generator::GenerateContext,
@@ -46,7 +46,7 @@ pub fn render_cjs<'code>(
   // So we determine the export mode (from auto) here and use it in the following code.
   let export_mode = match ctx.chunk.user_defined_entry_module(&ctx.link_output.module_table) {
     Some(entry_module) => {
-      let export_names = get_chunk_export_names(ctx.chunk, ctx.link_output, ctx.options);
+      let export_names = get_chunk_export_names_with_ctx(ctx);
       let has_default_export = export_names.iter().any(|name| name.as_str() == "default");
       let export_mode = determine_export_mode(warnings, ctx, entry_module, &export_names)?;
       // Only `named` export can we render the namespace markers.
diff --git a/crates/rolldown/src/ecmascript/format/esm.rs b/crates/rolldown/src/ecmascript/format/esm.rs
index a66ad9ccc872..144109e7f829 100644
--- a/crates/rolldown/src/ecmascript/format/esm.rs
+++ b/crates/rolldown/src/ecmascript/format/esm.rs
@@ -95,7 +95,6 @@ pub fn render_esm<'code>(
 
 fn render_esm_chunk_imports(ctx: &GenerateContext<'_>) -> Option<String> {
   let mut s = String::new();
-
   ctx.chunk.imports_from_other_chunks.iter().for_each(|(exporter_id, items)| {
     let importee_chunk = &ctx.chunk_graph.chunk_table[*exporter_id];
     let mut default_alias = vec![];
@@ -104,9 +103,9 @@ fn render_esm_chunk_imports(ctx: &GenerateContext<'_>) -> Option<String> {
       .filter_map(|item| {
         let canonical_ref = ctx.link_output.symbol_db.canonical_ref_for(item.import_ref);
         let imported = &ctx.chunk.canonical_names[&canonical_ref];
-        let Specifier::Literal(alias) = item.export_alias.as_ref().unwrap() else {
-          panic!("should not be star import from other chunks")
-        };
+        let alias = &ctx.render_export_items_index_vec[*exporter_id]
+          .get(&item.import_ref)
+          .expect("should have export item index")[0];
         if alias == imported {
           Some(alias.as_str().into())
         } else {
diff --git a/crates/rolldown/src/ecmascript/format/iife.rs b/crates/rolldown/src/ecmascript/format/iife.rs
index cbca6ed04a37..fa92632a4dff 100644
--- a/crates/rolldown/src/ecmascript/format/iife.rs
+++ b/crates/rolldown/src/ecmascript/format/iife.rs
@@ -25,7 +25,7 @@
 use crate::ecmascript::format::utils::namespace::generate_identifier;
 use crate::utils::chunk::namespace_marker::render_namespace_markers;
 use crate::utils::chunk::render_chunk_exports::{
-  get_chunk_export_names, render_wrapped_entry_chunk,
+  get_chunk_export_names_with_ctx, render_wrapped_entry_chunk,
 };
 use crate::{
   ecmascript::ecma_generator::RenderedModuleSources,
@@ -70,7 +70,7 @@ pub async fn render_iife<'code>(
   // iife wrapper start
 
   // Analyze the export information of the chunk.
-  let export_names = get_chunk_export_names(ctx.chunk, ctx.link_output, ctx.options);
+  let export_names = get_chunk_export_names_with_ctx(ctx);
   let has_exports = !export_names.is_empty();
   let has_default_export = export_names.iter().any(|name| name.as_str() == "default");
 
diff --git a/crates/rolldown/src/ecmascript/format/umd.rs b/crates/rolldown/src/ecmascript/format/umd.rs
index 8ecfc5bbbb88..d64bca1ad591 100644
--- a/crates/rolldown/src/ecmascript/format/umd.rs
+++ b/crates/rolldown/src/ecmascript/format/umd.rs
@@ -12,7 +12,7 @@ use crate::{
     determine_export_mode::determine_export_mode,
     namespace_marker::render_namespace_markers,
     render_chunk_exports::{
-      get_chunk_export_names, render_chunk_exports, render_wrapped_entry_chunk,
+      get_chunk_export_names_with_ctx, render_chunk_exports, render_wrapped_entry_chunk,
     },
   },
 };
@@ -43,7 +43,7 @@ pub async fn render_umd<'code>(
   // umd wrapper start
 
   // Analyze the export information of the chunk.
-  let export_names = get_chunk_export_names(ctx.chunk, ctx.link_output, ctx.options);
+  let export_names = get_chunk_export_names_with_ctx(ctx);
   let has_exports = !export_names.is_empty();
   let has_default_export = export_names.iter().any(|name| name.as_str() == "default");
 
diff --git a/crates/rolldown/src/stages/generate_stage/render_chunk_to_assets.rs b/crates/rolldown/src/stages/generate_stage/render_chunk_to_assets.rs
index b640e66c6596..56565948871d 100644
--- a/crates/rolldown/src/stages/generate_stage/render_chunk_to_assets.rs
+++ b/crates/rolldown/src/stages/generate_stage/render_chunk_to_assets.rs
@@ -4,12 +4,13 @@ use futures::future::try_join_all;
 use oxc_index::{IndexVec, index_vec};
 use rolldown_common::{
   Asset, EmittedChunkInfo, InstantiationKind, ModuleRenderArgs, ModuleRenderOutput, Output,
-  OutputAsset, OutputChunk, SharedFileEmitter, SourceMapType,
+  OutputAsset, OutputChunk, SharedFileEmitter, SourceMapType, SymbolRef,
 };
 use rolldown_error::{BuildDiagnostic, BuildResult};
+use rolldown_rstr::Rstr;
 use rolldown_utils::{
   concat_string,
-  indexmap::FxIndexSet,
+  indexmap::{FxIndexMap, FxIndexSet},
   rayon::{IntoParallelRefIterator, ParallelIterator},
 };
 
@@ -22,8 +23,10 @@ use crate::{
   type_alias::{IndexAssets, IndexChunkToAssets, IndexInstantiatedChunks},
   types::generator::{GenerateContext, Generator},
   utils::{
-    augment_chunk_hash::augment_chunk_hash, chunk::finalize_chunks::finalize_assets,
-    process_code_and_sourcemap::process_code_and_sourcemap, render_chunks::render_chunks,
+    augment_chunk_hash::augment_chunk_hash,
+    chunk::{finalize_chunks::finalize_assets, render_chunk_exports::get_export_items},
+    process_code_and_sourcemap::process_code_and_sourcemap,
+    render_chunks::render_chunks,
   },
 };
 
@@ -182,6 +185,18 @@ impl GenerateStage<'_> {
     let mut index_preliminary_assets: IndexInstantiatedChunks =
       IndexVec::with_capacity(chunk_graph.chunk_table.len());
     let chunk_index_to_codegen_rets = self.create_chunk_to_codegen_ret_map(chunk_graph);
+    let render_export_items_index_vec = &chunk_graph
+      .chunk_table
+      .chunks
+      .iter()
+      .map(|item| {
+        let mut map: FxIndexMap<SymbolRef, Vec<Rstr>> = FxIndexMap::default();
+        get_export_items(item, self.link_output, self.options).into_iter().for_each(|(k, v)| {
+          map.entry(v).or_default().push(k);
+        });
+        map
+      })
+      .collect();
 
     try_join_all(
       chunk_graph
@@ -199,6 +214,7 @@ impl GenerateStage<'_> {
             plugin_driver: self.plugin_driver,
             warnings: vec![],
             module_id_to_codegen_ret,
+            render_export_items_index_vec,
           };
           let ecma_chunks = EcmaGenerator::instantiate_chunk(&mut ctx).await;
 
@@ -212,6 +228,7 @@ impl GenerateStage<'_> {
             warnings: vec![],
             // FIXME: module_id_to_codegen_ret is currently not used in CssGenerator. But we need to pass it to satisfy the args.
             module_id_to_codegen_ret: vec![],
+            render_export_items_index_vec: &index_vec![],
           };
           let css_chunks = CssGenerator::instantiate_chunk(&mut ctx).await;
 
@@ -225,6 +242,7 @@ impl GenerateStage<'_> {
             warnings: vec![],
             // FIXME: module_id_to_codegen_ret is currently not used in AssetGenerator. But we need to pass it to satisfy the args.
             module_id_to_codegen_ret: vec![],
+            render_export_items_index_vec: &index_vec![],
           };
           let asset_chunks = AssetGenerator::instantiate_chunk(&mut ctx).await;
 
diff --git a/crates/rolldown/src/types/generator.rs b/crates/rolldown/src/types/generator.rs
index 7571e4c7bf6a..8a743a080cd8 100644
--- a/crates/rolldown/src/types/generator.rs
+++ b/crates/rolldown/src/types/generator.rs
@@ -1,9 +1,11 @@
+use oxc_index::IndexVec;
 use rolldown_common::{
   Chunk, ChunkIdx, InstantiatedChunk, ModuleRenderOutput, NormalizedBundlerOptions, SymbolRef,
 };
 use rolldown_error::{BuildDiagnostic, BuildResult};
 use rolldown_plugin::SharedPluginDriver;
 use rolldown_rstr::Rstr;
+use rolldown_utils::indexmap::FxIndexMap;
 use rustc_hash::FxHashMap;
 
 use crate::{chunk_graph::ChunkGraph, stages::link_stage::LinkStageOutput};
@@ -17,6 +19,14 @@ pub struct GenerateContext<'a> {
   pub plugin_driver: &'a SharedPluginDriver,
   pub warnings: Vec<BuildDiagnostic>,
   pub module_id_to_codegen_ret: Vec<Option<ModuleRenderOutput>>,
+  /// The key of the map is exported item symbol,
+  /// the value of the map is optional alias. e.g.
+  /// - chunkB.js
+  /// ```js
+  /// export const a = 10000000;
+  /// export {a as b}; // symbol_ref points to `a`, and alias is `b`
+  /// ```
+  pub render_export_items_index_vec: &'a IndexVec<ChunkIdx, FxIndexMap<SymbolRef, Vec<Rstr>>>,
 }
 
 impl GenerateContext<'_> {
diff --git a/crates/rolldown/src/utils/chunk/mod.rs b/crates/rolldown/src/utils/chunk/mod.rs
index 65135bff1645..af9cb19d2912 100644
--- a/crates/rolldown/src/utils/chunk/mod.rs
+++ b/crates/rolldown/src/utils/chunk/mod.rs
@@ -1,3 +1,4 @@
+use self::render_chunk_exports::get_chunk_export_names;
 use rolldown_common::{
   Chunk, ChunkKind, ModuleId, NormalizedBundlerOptions, RenderedModule, RollupPreRenderedChunk,
   RollupRenderedChunk,
@@ -6,8 +7,6 @@ use rustc_hash::FxHashMap;
 
 use crate::{stages::link_stage::LinkStageOutput, types::generator::GenerateContext};
 
-use self::render_chunk_exports::get_chunk_export_names;
-
 pub mod deconflict_chunk_symbols;
 pub mod determine_export_mode;
 pub mod determine_use_strict;
diff --git a/crates/rolldown/src/utils/chunk/render_chunk_exports.rs b/crates/rolldown/src/utils/chunk/render_chunk_exports.rs
index 61242ce8897f..aec7ee19f6db 100644
--- a/crates/rolldown/src/utils/chunk/render_chunk_exports.rs
+++ b/crates/rolldown/src/utils/chunk/render_chunk_exports.rs
@@ -1,6 +1,7 @@
 use std::borrow::Cow;
 use std::fmt::Write as _;
 
+use itertools::Itertools;
 use rolldown_common::{
   Chunk, ChunkKind, EntryPointKind, ExportsKind, IndexModules, ModuleIdx, NormalizedBundlerOptions,
   OutputExports, OutputFormat, SymbolRef, SymbolRefDb, WrapKind,
@@ -88,7 +89,11 @@ pub fn render_chunk_exports(
   export_mode: Option<&OutputExports>,
 ) -> Option<String> {
   let GenerateContext { chunk, link_output, options, .. } = ctx;
-  let export_items = get_export_items(chunk, link_output, options).into_iter().collect::<Vec<_>>();
+  let export_items: Vec<(Rstr, SymbolRef)> = ctx.render_export_items_index_vec[ctx.chunk_idx]
+    .clone()
+    .into_iter()
+    .flat_map(|(symbol_ref, names)| names.into_iter().map(|name| (name, symbol_ref)).collect_vec())
+    .collect();
 
   match options.format {
     OutputFormat::Esm => {
@@ -351,6 +356,17 @@ pub fn get_chunk_export_names(
     .collect::<Vec<_>>()
 }
 
+pub fn get_chunk_export_names_with_ctx(ctx: &GenerateContext<'_>) -> Vec<Rstr> {
+  let GenerateContext { chunk, link_output, render_export_items_index_vec, .. } = ctx;
+  if let ChunkKind::EntryPoint { module: entry_id, .. } = &chunk.kind {
+    let entry_meta = &link_output.metas[*entry_id];
+    if matches!(entry_meta.wrap_kind, WrapKind::Cjs) {
+      return vec![Rstr::new("default")];
+    }
+  }
+  render_export_items_index_vec[ctx.chunk_idx].values().flatten().cloned().collect::<Vec<_>>()
+}
+
 fn must_keep_live_binding(
   export_ref: SymbolRef,
   symbol_db: &SymbolRefDb,
diff --git a/crates/rolldown/tests/rolldown/misc/preserve_modules/_config.json b/crates/rolldown/tests/rolldown/misc/preserve_modules/_config.json
new file mode 100644
index 000000000000..e35a629a436a
--- /dev/null
+++ b/crates/rolldown/tests/rolldown/misc/preserve_modules/_config.json
@@ -0,0 +1,19 @@
+{
+  "config": {
+    "input": [
+      {
+        "name": "entry",
+        "import": "./main.js"
+      },
+      {
+        "name": "a",
+        "import": "./a.js"
+      },
+      {
+        "name": "b",
+        "import": "./b.js"
+      }
+    ],
+    "preserveModules": true
+  }
+}
diff --git a/crates/rolldown/tests/rolldown/misc/preserve_modules/_test.mjs b/crates/rolldown/tests/rolldown/misc/preserve_modules/_test.mjs
new file mode 100644
index 000000000000..3d42d83f5a94
--- /dev/null
+++ b/crates/rolldown/tests/rolldown/misc/preserve_modules/_test.mjs
@@ -0,0 +1,5 @@
+import { B } from './dist/main.js'
+import assert from 'node:assert'
+
+assert.strictEqual(B, 100)
+
diff --git a/crates/rolldown/tests/rolldown/misc/preserve_modules/a.ts b/crates/rolldown/tests/rolldown/misc/preserve_modules/a.ts
new file mode 100644
index 000000000000..0176c6b50ebc
--- /dev/null
+++ b/crates/rolldown/tests/rolldown/misc/preserve_modules/a.ts
@@ -0,0 +1,3 @@
+import B from "./b";
+
+export { B };
diff --git a/crates/rolldown/tests/rolldown/misc/preserve_modules/artifacts.snap b/crates/rolldown/tests/rolldown/misc/preserve_modules/artifacts.snap
new file mode 100644
index 000000000000..d9110ed5ec5b
--- /dev/null
+++ b/crates/rolldown/tests/rolldown/misc/preserve_modules/artifacts.snap
@@ -0,0 +1,29 @@
+---
+source: crates/rolldown_testing/src/integration_test.rs
+---
+# Assets
+
+## a.js
+
+```js
+import b_default from "./b.js";
+
+export { b_default as B };
+```
+## b.js
+
+```js
+//#region b.ts
+const b = 100;
+var b_default = b;
+
+//#endregion
+export { b_default as default };
+```
+## main.js
+
+```js
+import b_default from "./b.js";
+
+export { b_default as B };
+```
diff --git a/crates/rolldown/tests/rolldown/misc/preserve_modules/b.ts b/crates/rolldown/tests/rolldown/misc/preserve_modules/b.ts
new file mode 100644
index 000000000000..97ad6f4a1d72
--- /dev/null
+++ b/crates/rolldown/tests/rolldown/misc/preserve_modules/b.ts
@@ -0,0 +1,2 @@
+const b = 100;
+export default b;
diff --git a/crates/rolldown/tests/rolldown/misc/preserve_modules/main.ts b/crates/rolldown/tests/rolldown/misc/preserve_modules/main.ts
new file mode 100644
index 000000000000..21c83d4780fc
--- /dev/null
+++ b/crates/rolldown/tests/rolldown/misc/preserve_modules/main.ts
@@ -0,0 +1 @@
+export * from './a'
diff --git a/crates/rolldown/tests/snapshots/integration_rolldown__filename_with_hash.snap b/crates/rolldown/tests/snapshots/integration_rolldown__filename_with_hash.snap
index 57a30eee561b..b6a8c4544d4a 100644
--- a/crates/rolldown/tests/snapshots/integration_rolldown__filename_with_hash.snap
+++ b/crates/rolldown/tests/snapshots/integration_rolldown__filename_with_hash.snap
@@ -4786,6 +4786,12 @@ expression: output
 
 - main-!~{000}~.js => main-Cv6xMI1a.js
 
+# tests/rolldown/misc/preserve_modules
+
+- a-!~{001}~.js => a-9EeU3Xa2.js
+- b-!~{002}~.js => b-_WUCLyHa.js
+- main-!~{000}~.js => main-Bq02Itoe.js
+
 # tests/rolldown/misc/reexport_star
 
 - entry-!~{001}~.js => entry-9pAfnsAm.js


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adjusts how export items are tracked and rendered to support `preserveModules` alongside multiple entry points. Key changes include  
- Introducing `render_export_items_index_vec` to cache export names by symbol  
- Replacing direct calls to `get_export_items` with context-aware versions in various formatters  
- Adding and cleaning up imports, utilities, and example configurations

### Reviewed Changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 7 comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| crates/rolldown/tests/esbuild/default/forbid_string_export_names_no_bundle/artifacts.snap | Snapshot updated to remove `snapshot_kind` metadata |
| crates/rolldown/src/utils/chunk/render_chunk_exports.rs | Added `get_chunk_export_names_with_ctx` and replaced export item collection |
| crates/rolldown/src/utils/chunk/mod.rs | Adjusted import of `get_chunk_export_names` |
| crates/rolldown/src/types/generator.rs | Added `render_export_items_index_vec` to `GenerateContext` |
| crates/rolldown/src/stages/generate_stage/render_chunk_to_assets.rs | Built the export-items index vector and wired it into generators |
| crates/rolldown/src/stages/generate_stage/compute_cross_chunk_links.rs | Debug statements added around import alias resolution |
| crates/rolldown/src/ecmascript/format/umd.rs | Switched to `get_chunk_export_names_with_ctx` |
| crates/rolldown/src/ecmascript/format/iife.rs | Switched to `get_chunk_export_names_with_ctx` |
| crates/rolldown/src/ecmascript/format/esm.rs | Added debug prints and switched to context-based export names |
| crates/rolldown/src/ecmascript/format/cjs.rs | Switched to context-based export names |
| crates/rolldown/examples/basic/entry.js | Simplified to a single export entry |
| crates/rolldown/examples/basic/a.js, b.js | Added modules demonstrating aliased and default exports |
| crates/rolldown/examples/basic.rs | Commented out `sourcemap` and `preserve_modules` in example config |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**crates/rolldown/tests/esbuild/default/forbid_string_export_names_no_bundle/artifacts.snap:2**
* The removal of `snapshot_kind` requires updating the test harness; ensure this snapshot change is intentional and tests still validate the expected output.
```
-snapshot_kind: text
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T08:12:56Z
user: login: IWANABETHATGUY
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T08:18:20Z
user: login: IWANABETHATGUY
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T08:29:08Z
user: login: IWANABETHATGUY
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T08:31:05Z
user: login: hyf0
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-21T09:15:28Z

Review comments:
diff_hunk: @@ -182,6 +185,19 @@ impl GenerateStage<'_> {
     let mut index_preliminary_assets: IndexInstantiatedChunks =
       IndexVec::with_capacity(chunk_graph.chunk_table.len());
     let chunk_index_to_codegen_rets = self.create_chunk_to_codegen_ret_map(chunk_graph);
+    let render_export_items_index_vec = &chunk_graph
+      .chunk_table
+      .chunks
+      .iter()
+      .map(|item| {
+        let mut map: FxIndexMap<SymbolRef, Vec<Rstr>> = FxIndexMap::default();
+        get_export_items(item, &self.link_output, &self.options).into_iter().for_each(|(k, v)| {
+          map.entry(v).or_default().push(k);
+        });
+        map
+      })
+      .collect();
+    dbg!(&render_export_items_index_vec);
path: crates/rolldown/src/stages/generate_stage/render_chunk_to_assets.rs
user: login: Copilot
body: Remove this `dbg!` call; it was likely used for debugging and will clutter logs in production.
```suggestion

```
created_at: 2025-05-21T08:12:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -308,6 +308,8 @@ impl GenerateStage<'_> {
   ) {
     chunk_graph.chunk_table.iter_enumerated().for_each(|(chunk_id, chunk)| {
       let chunk_meta_imports = &index_chunk_depended_symbols[chunk_id];
+      dbg!(&chunk.entry_module(&self.link_output.module_table).and_then(|item| { Some(&item.id) }));
+      dbg!(&chunk_meta_imports);
path: crates/rolldown/src/stages/generate_stage/compute_cross_chunk_links.rs
user: login: Copilot
body: This debug statement should be removed before merging to avoid unintended console output.
```suggestion
      chunk.entry_module(&self.link_output.module_table).and_then(|item| { Some(&item.id) });
      // Removed debug statement for chunk_meta_imports.
```
created_at: 2025-05-21T08:12:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -308,6 +308,8 @@ impl GenerateStage<'_> {
   ) {
     chunk_graph.chunk_table.iter_enumerated().for_each(|(chunk_id, chunk)| {
       let chunk_meta_imports = &index_chunk_depended_symbols[chunk_id];
+      dbg!(&chunk.entry_module(&self.link_output.module_table).and_then(|item| { Some(&item.id) }));
+      dbg!(&chunk_meta_imports);
path: crates/rolldown/src/stages/generate_stage/compute_cross_chunk_links.rs
user: login: Copilot
body: Leftover `dbg!` should be removed to clean up debug logging in the cross-chunk link computation.
```suggestion

```
created_at: 2025-05-21T08:12:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -95,8 +95,9 @@ pub fn render_esm<'code>(
 
 fn render_esm_chunk_imports(ctx: &GenerateContext<'_>) -> Option<String> {
   let mut s = String::new();
-
+  println!("imports of {:?}", ctx.chunk.name);
path: crates/rolldown/src/ecmascript/format/esm.rs
user: login: Copilot
body: This `println!` is likely a debug artifact and should be removed as it will print during normal operation.
```suggestion

```
created_at: 2025-05-21T08:12:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -95,8 +95,9 @@ pub fn render_esm<'code>(
 
 fn render_esm_chunk_imports(ctx: &GenerateContext<'_>) -> Option<String> {
   let mut s = String::new();
-
+  println!("imports of {:?}", ctx.chunk.name);
   ctx.chunk.imports_from_other_chunks.iter().for_each(|(exporter_id, items)| {
+    dbg!(&items);
path: crates/rolldown/src/ecmascript/format/esm.rs
user: login: Copilot
body: Remove this `dbg!` to prevent accidental debug output in the final bundle process.
```suggestion

```
created_at: 2025-05-21T08:12:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,3 +1,4 @@
+use render_chunk_exports::get_chunk_export_names;
path: crates/rolldown/src/utils/chunk/mod.rs
user: login: Copilot
body: [nitpick] Consider using an explicit path (e.g., `use self::render_chunk_exports::get_chunk_export_names;`) to make the import origin clearer.
```suggestion
use self::render_chunk_exports::get_chunk_export_names;
```
created_at: 2025-05-21T08:12:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,11 +9,12 @@ async fn main() {
   let mut bundler = Bundler::new(BundlerOptions {
     input: Some(vec![
       "./entry.js".to_string().into(),
-      InputItem { import: "./other-entry.js".to_string(), ..Default::default() },
-      InputItem { name: Some("third-entry".to_string()), import: "./third-entry.js".to_string() },
+      InputItem { import: "./a.js".to_string(), ..Default::default() },
+      InputItem { import: "./b.js".to_string(), ..Default::default() },
     ]),
     cwd: Some(workspace::crate_dir("rolldown").join("./examples/basic").normalize()),
-    sourcemap: Some(SourceMapType::File),
+    // sourcemap: Some(SourceMapType::File),
+    // preserve_modules: Some(true),
path: crates/rolldown/examples/basic.rs
user: login: Copilot
body: The example config comments out key options (`sourcemap`, `preserve_modules`); please update or document why these are disabled to avoid confusion for new users.
```suggestion
    // sourcemap: Some(SourceMapType::File), // Uncomment to generate a source map file for debugging purposes.
    // preserve_modules: Some(true), // Uncomment to preserve the module structure instead of bundling into a single file.
```
created_at: 2025-05-21T08:12:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -182,6 +185,18 @@ impl GenerateStage<'_> {
     let mut index_preliminary_assets: IndexInstantiatedChunks =
       IndexVec::with_capacity(chunk_graph.chunk_table.len());
     let chunk_index_to_codegen_rets = self.create_chunk_to_codegen_ret_map(chunk_graph);
+    let render_export_items_index_vec = &chunk_graph
path: crates/rolldown/src/stages/generate_stage/render_chunk_to_assets.rs
user: login: IWANABETHATGUY
body: Pre generate the `export_items_index_vec`, because now render chunk_imports may use other chunk `export_item` info
created_at: 2025-05-21T08:18:20Z
author_association: MEMBER
diff_hunk: @@ -104,9 +103,9 @@ fn render_esm_chunk_imports(ctx: &GenerateContext<'_>) -> Option<String> {
       .filter_map(|item| {
         let canonical_ref = ctx.link_output.symbol_db.canonical_ref_for(item.import_ref);
         let imported = &ctx.chunk.canonical_names[&canonical_ref];
-        let Specifier::Literal(alias) = item.export_alias.as_ref().unwrap() else {
-          panic!("should not be star import from other chunks")
-        };
+        let alias = &ctx.render_export_items_index_vec[*exporter_id]
path: crates/rolldown/src/ecmascript/format/esm.rs
user: login: IWANABETHATGUY
body: In some case the `export_alias` can't represent the alias correctly, so I use info from imported chunk `exports_info` instead, use the test case as an example: 

```bash
[crates/rolldown/src/ecmascript/format/esm.rs:99:5] &ctx.chunk.name = Some(
    "main",
)
[crates/rolldown/src/ecmascript/format/esm.rs:105:9] &item = CrossChunkImportItem {
    export_alias: Some(
        Literal(
            Rstr(
                "b_default",  // the alias should be `default` but here it return `b_default`
            ),
        ),
    ),
    import_ref: SymbolRef {
        owner: 3,
        symbol: SymbolId(
            4294967294,
        ),
    },
}
[crates/rolldown/src/ecmascript/format/esm.rs:108:9] &imported = Rstr(
    "b_default",
)
[crates/rolldown/src/ecmascript/format/esm.rs:99:5] &ctx.chunk.name = Some(
    "a",
)
[crates/rolldown/src/ecmascript/format/esm.rs:105:9] &item = CrossChunkImportItem {
    export_alias: Some(
        Literal(
            Rstr(
                "b_default",
            ),
        ),
    ),
    import_ref: SymbolRef {
        owner: 3,
        symbol: SymbolId(
            4294967294,
        ),
    },
}
[crates/rolldown/src/ecmascript/format/esm.rs:108:9] &imported = Rstr(
    "b_default",
)
```
created_at: 2025-05-21T08:29:07Z
author_association: MEMBER
diff_hunk: @@ -104,9 +103,9 @@ fn render_esm_chunk_imports(ctx: &GenerateContext<'_>) -> Option<String> {
       .filter_map(|item| {
         let canonical_ref = ctx.link_output.symbol_db.canonical_ref_for(item.import_ref);
         let imported = &ctx.chunk.canonical_names[&canonical_ref];
-        let Specifier::Literal(alias) = item.export_alias.as_ref().unwrap() else {
-          panic!("should not be star import from other chunks")
-        };
+        let alias = &ctx.render_export_items_index_vec[*exporter_id]
+          .get(&item.import_ref)
+          .expect("should have export item index")[0];
path: crates/rolldown/src/ecmascript/format/esm.rs
user: login: IWANABETHATGUY
body: Same symbol may have multiple alias, e.g.: 

```js
const a = 1000;
export {
  a, 
  a as b
}
```
always use the same alias is alright, since the both point to the same `symbol` finally.
created_at: 2025-05-21T08:31:05Z
author_association: MEMBER

PR comments:
user: login: netlify[bot]
created_at: 2025-05-21T08:10:08Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *rolldown-rs* canceled.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 60a424265015ec78a8b206c85bed36a729a7ca2c |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/projects/rolldown-rs/deploys/682d8fbb3ffae40008941999 |
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-21T08:47:38Z
body: ### Benchmarks Rust
- target: `main`(0312f66401f70a0de261c8a88953f1e6c3227a38)
- pr: `fix-preserve-module-and-multi-entry`(60a424265015ec78a8b206c85bed36a729a7ca2c)
```
group                                                               pr                                     target
-----                                                               --                                     ------
bundle/bundle@multi-duplicated-top-level-symbol                     1.02     85.6±1.95ms        ? ?/sec    1.00     84.2±2.63ms        ? ?/sec
bundle/bundle@multi-duplicated-top-level-symbol-minify              1.03    106.8±2.61ms        ? ?/sec    1.00    103.3±2.53ms        ? ?/sec
bundle/bundle@multi-duplicated-top-level-symbol-minify-sourcemap    1.00    115.6±2.41ms        ? ?/sec    1.03    119.3±1.38ms        ? ?/sec
bundle/bundle@multi-duplicated-top-level-symbol-sourcemap           1.00     93.8±1.06ms        ? ?/sec    1.02     95.3±1.79ms        ? ?/sec
bundle/bundle@rome-ts                                               1.00    125.4±2.79ms        ? ?/sec    1.01    126.9±1.96ms        ? ?/sec
bundle/bundle@rome-ts-minify                                        1.01    220.4±3.27ms        ? ?/sec    1.00    217.4±4.39ms        ? ?/sec
bundle/bundle@rome-ts-minify-sourcemap                              1.02    257.3±3.77ms        ? ?/sec    1.00    251.3±4.15ms        ? ?/sec
bundle/bundle@rome-ts-sourcemap                                     1.00    136.9±1.72ms        ? ?/sec    1.00    137.0±1.78ms        ? ?/sec
bundle/bundle@threejs                                               1.00     42.1±0.94ms        ? ?/sec    1.03     43.2±1.62ms        ? ?/sec
bundle/bundle@threejs-minify                                        1.02     91.0±1.88ms        ? ?/sec    1.00     89.0±3.43ms        ? ?/sec
bundle/bundle@threejs-minify-sourcemap                              1.10    115.5±2.26ms        ? ?/sec    1.00    105.0±1.64ms        ? ?/sec
bundle/bundle@threejs-sourcemap                                     1.00     48.3±0.25ms        ? ?/sec    1.01     48.8±0.95ms        ? ?/sec
bundle/bundle@threejs10x                                            1.00    437.4±8.33ms        ? ?/sec    1.00    436.7±4.79ms        ? ?/sec
bundle/bundle@threejs10x-minify                                     1.00   1031.2±7.83ms        ? ?/sec    1.00   1034.9±7.18ms        ? ?/sec
bundle/bundle@threejs10x-minify-sourcemap                           1.00   1225.5±8.22ms        ? ?/sec    1.00  1222.3±10.50ms        ? ?/sec
bundle/bundle@threejs10x-sourcemap                                  1.00    506.5±7.38ms        ? ?/sec    1.00    505.2±3.19ms        ? ?/sec
remapping/remapping                                                 1.00     22.8±0.12ms        ? ?/sec    1.19     27.2±0.39ms        ? ?/sec
remapping/render-chunk-remapping                                    1.01     68.3±1.86ms        ? ?/sec    1.00     67.6±3.68ms        ? ?/sec
scan/scan@rome-ts                                                   1.02     95.0±1.37ms        ? ?/sec    1.00     93.5±1.16ms        ? ?/sec
scan/scan@threejs                                                   1.00     31.8±1.21ms        ? ?/sec    1.00     31.8±1.15ms        ? ?/sec
scan/scan@threejs10x                                                1.01    315.1±3.27ms        ? ?/sec    1.00    312.4±3.73ms        ? ?/sec
```
author_association: CONTRIBUTOR
user: login: IWANABETHATGUY
created_at: 2025-05-21T09:22:06Z
body: * **#4628** <a href="https://app.graphite.dev/github/pr/rolldown/rolldown/4628?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#4626** <a href="https://app.graphite.dev/github/pr/rolldown/rolldown/4626?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/rolldown/rolldown/4626?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* `main`


<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: MEMBER