Repository: rolldown/rolldown
Pull request #4460: refactor(hmr): hmr chunk loadExports
Author: login: underfin
Labels: name: needs-triage
Created: 2025-05-09T09:31:48Z | Merged: 2025-05-10T05:25:08Z

<!-- Thank you for contributing! -->

### Description

The pr intend to support some features.
- avoid create import one module mulitly times
- interop
- import external module, even it's not used at now.
<!-- Please insert your description here and provide especially info about the "what" this PR is solving -->



Diff:
diff --git a/crates/rolldown/src/hmr/hmr_ast_finalizer.rs b/crates/rolldown/src/hmr/hmr_ast_finalizer.rs
index c232d0344282..3449d8d60491 100644
--- a/crates/rolldown/src/hmr/hmr_ast_finalizer.rs
+++ b/crates/rolldown/src/hmr/hmr_ast_finalizer.rs
@@ -2,19 +2,19 @@ use oxc::{
   allocator::{Allocator, Box as ArenaBox, IntoIn, TakeIn},
   ast::{
     NONE,
-    ast::{self, ExportDefaultDeclarationKind, Expression, ObjectPropertyKind},
+    ast::{self, ExportDefaultDeclarationKind, Expression, ObjectPropertyKind, Statement},
   },
   ast_visit::{VisitMut, walk_mut},
   semantic::{Scoping, SymbolId},
-  span::SPAN,
+  span::{SPAN, Span},
 };
 
 use rolldown_common::{IndexModules, Module, ModuleIdx, NormalModule};
 use rolldown_ecmascript_utils::{
-  AstSnippet, BindingIdentifierExt, ExpressionExt, quote_stmt, quote_stmts,
+  AstSnippet, BindingIdentifierExt, ExpressionExt, quote_expr, quote_stmt, quote_stmts,
 };
 use rolldown_utils::indexmap::FxIndexSet;
-use rustc_hash::FxHashMap;
+use rustc_hash::{FxHashMap, FxHashSet};
 
 pub struct HmrAstFinalizer<'me, 'ast> {
   // Outside input
@@ -28,6 +28,7 @@ pub struct HmrAstFinalizer<'me, 'ast> {
   pub import_binding: FxHashMap<SymbolId, String>,
   pub exports: oxc::allocator::Vec<'ast, ObjectPropertyKind<'ast>>,
   pub dependencies: FxIndexSet<ModuleIdx>,
+  pub imports: FxHashSet<ModuleIdx>,
 }
 
 impl<'ast> HmrAstFinalizer<'_, 'ast> {
@@ -153,6 +154,37 @@ impl<'ast> HmrAstFinalizer<'_, 'ast> {
       *expr = self.snippet.id_ref_expr(&hot_name, SPAN);
     }
   }
+
+  fn create_binding_name(importee: &Module) -> String {
+    format!("import_{}", importee.repr_name())
+  }
+
+  fn create_load_exports_call_stmt(
+    &mut self,
+    importee: &Module,
+    binding_name: &str,
+    span: Span,
+  ) -> Option<Statement<'ast>> {
+    if self.imports.contains(&importee.idx()) {
+      return None;
+    }
+    self.imports.insert(importee.idx());
+
+    let id = &importee.stable_id();
+    let interop = match importee {
+      Module::Normal(importee) => self.module.interop(importee),
+      Module::External(_) => None,
+    };
+    let call_expr =
+      quote_expr(self.alloc, format!("__rolldown_runtime__.loadExports({id:?});",).as_str());
+
+    let stmt = self.snippet.variable_declarator_require_call_stmt(
+      binding_name,
+      self.snippet.to_esm_call_with_interop("__rolldown_runtime__.__toESM", call_expr, interop),
+      span,
+    );
+    Some(stmt)
+  }
 }
 
 impl<'ast> VisitMut<'ast> for HmrAstFinalizer<'_, 'ast> {
@@ -265,47 +297,38 @@ impl<'ast> VisitMut<'ast> for HmrAstFinalizer<'_, 'ast> {
           // ```
           let rec_id = self.module.imports[&import_decl.span];
           let rec = &self.module.import_records[rec_id];
-          match &self.modules[rec.resolved_module] {
-            Module::Normal(importee) => {
-              self.dependencies.insert(rec.resolved_module);
-              let id = &importee.stable_id;
-              let binding_name = format!("import_{}", importee.repr_name);
-              let stmt = quote_stmt(
-                self.alloc,
-                format!("const {binding_name} = __rolldown_runtime__.loadExports({id:?});",)
-                  .as_str(),
-              );
-              import_decl.specifiers.as_ref().inspect(|specifiers| {
-                specifiers.iter().for_each(|spec| match spec {
-                  ast::ImportDeclarationSpecifier::ImportSpecifier(import_specifier) => {
-                    self.import_binding.insert(
-                      import_specifier.local.expect_symbol_id(),
-                      format!("{binding_name}.{}", import_specifier.imported.name()),
-                    );
-                  }
-                  ast::ImportDeclarationSpecifier::ImportDefaultSpecifier(
-                    import_default_specifier,
-                  ) => {
-                    self.import_binding.insert(
-                      import_default_specifier.local.expect_symbol_id(),
-                      format!("{binding_name}.default"),
-                    );
-                  }
-                  ast::ImportDeclarationSpecifier::ImportNamespaceSpecifier(
-                    import_namespace_specifier,
-                  ) => {
-                    self.import_binding.insert(
-                      import_namespace_specifier.local.expect_symbol_id(),
-                      binding_name.to_string(),
-                    );
-                  }
-                });
-              });
-              *node = stmt;
-            }
-            Module::External(_importee) => {
-              todo!("handle external module");
-            }
+          let importee = &self.modules[rec.resolved_module];
+          self.dependencies.insert(rec.resolved_module);
+
+          let binding_name = Self::create_binding_name(importee);
+          import_decl.specifiers.as_ref().inspect(|specifiers| {
+            specifiers.iter().for_each(|spec| match spec {
+              ast::ImportDeclarationSpecifier::ImportSpecifier(import_specifier) => {
+                self.import_binding.insert(
+                  import_specifier.local.expect_symbol_id(),
+                  format!("{binding_name}.{}", import_specifier.imported.name()),
+                );
+              }
+              ast::ImportDeclarationSpecifier::ImportDefaultSpecifier(import_default_specifier) => {
+                self.import_binding.insert(
+                  import_default_specifier.local.expect_symbol_id(),
+                  format!("{binding_name}.default"),
+                );
+              }
+              ast::ImportDeclarationSpecifier::ImportNamespaceSpecifier(
+                import_namespace_specifier,
+              ) => {
+                self.import_binding.insert(
+                  import_namespace_specifier.local.expect_symbol_id(),
+                  binding_name.to_string(),
+                );
+              }
+            });
+          });
+          if let Some(stmt) =
+            self.create_load_exports_call_stmt(importee, &binding_name, import_decl.span)
+          {
+            *node = stmt;
           }
         }
         ast::ModuleDeclaration::ExportNamedDeclaration(decl) => {
@@ -313,58 +336,49 @@ impl<'ast> VisitMut<'ast> for HmrAstFinalizer<'_, 'ast> {
             // export {} from '...'
             let rec_id = self.module.imports[&decl.span];
             let rec = &self.module.import_records[rec_id];
+            let importee = &self.modules[rec.resolved_module];
             self.dependencies.insert(rec.resolved_module);
-            match &self.modules[rec.resolved_module] {
-              Module::Normal(importee) => {
-                self.dependencies.insert(rec.resolved_module);
-                let id = &importee.stable_id;
-                let binding_name = format!("import_{}", importee.repr_name);
-                let stmt = quote_stmt(
-                  self.alloc,
-                  format!("const {binding_name} = __rolldown_runtime__.loadExports({id:?});",)
-                    .as_str(),
-                );
-                self.exports.extend(decl.specifiers.iter().map(|specifier| {
-                  self.snippet.object_property_kind_object_property(
-                    &specifier.exported.name(),
-                    match &specifier.local {
-                      ast::ModuleExportName::IdentifierName(ident) => {
-                        Expression::StaticMemberExpression(
-                          self.snippet.builder.alloc_static_member_expression(
-                            SPAN,
-                            self.snippet.id_ref_expr(&binding_name, SPAN),
-                            self.snippet.builder.identifier_name(SPAN, ident.name.as_str()),
-                            false,
-                          ),
-                        )
-                      }
-                      ast::ModuleExportName::StringLiteral(str) => {
-                        Expression::ComputedMemberExpression(
-                          self.snippet.builder.alloc_computed_member_expression(
-                            SPAN,
-                            self.snippet.id_ref_expr(&binding_name, SPAN),
-                            self.snippet.builder.expression_string_literal(
-                              SPAN, str.value.as_str(), None
-                            ),
-                            false,
-                          ),
-                        )
-                      }
-                      ast::ModuleExportName::IdentifierReference(_) => {
-                        unreachable!(
-                          "ModuleExportName IdentifierReference is invalid in ExportNamedDeclaration with source"
-                        )
-                      }
-                    },
-                    matches!(specifier.exported, ast::ModuleExportName::StringLiteral(_))
-                  )
-                }));
 
-                *node = stmt;
-              }
-              Module::External(_importee) => {
-                todo!("handle external module");
-              }
+            let binding_name = Self::create_binding_name(importee);
+            self.exports.extend(decl.specifiers.iter().map(|specifier| {
+              self.snippet.object_property_kind_object_property(
+                &specifier.exported.name(),
+                match &specifier.local {
+                  ast::ModuleExportName::IdentifierName(ident) => {
+                    Expression::StaticMemberExpression(
+                      self.snippet.builder.alloc_static_member_expression(
+                        SPAN,
+                        self.snippet.id_ref_expr(&binding_name, SPAN),
+                        self.snippet.builder.identifier_name(SPAN, ident.name.as_str()),
+                        false,
+                      ),
+                    )
+                  }
+                  ast::ModuleExportName::StringLiteral(str) => {
+                    Expression::ComputedMemberExpression(
+                      self.snippet.builder.alloc_computed_member_expression(
+                        SPAN,
+                        self.snippet.id_ref_expr(&binding_name, SPAN),
+                        self.snippet.builder.expression_string_literal(
+                          SPAN, str.value.as_str(), None
+                        ),
+                        false,
+                      ),
+                    )
+                  }
+                  ast::ModuleExportName::IdentifierReference(_) => {
+                    unreachable!(
+                      "ModuleExportName IdentifierReference is invalid in ExportNamedDeclaration with source"
+                    )
+                  }
+                },
+                matches!(specifier.exported, ast::ModuleExportName::StringLiteral(_))
+              )
+            }));
+            if let Some(stmt) =
+              self.create_load_exports_call_stmt(importee, &binding_name, decl.span)
+            {
+              *node = stmt;
             }
           } else if let Some(decl) = &mut decl.declaration {
             match decl {
diff --git a/crates/rolldown/src/hmr/hmr_manager.rs b/crates/rolldown/src/hmr/hmr_manager.rs
index fe46dd097bbf..68c5c7f428bd 100644
--- a/crates/rolldown/src/hmr/hmr_manager.rs
+++ b/crates/rolldown/src/hmr/hmr_manager.rs
@@ -314,6 +314,7 @@ impl HmrManager {
           exports: oxc::allocator::Vec::new_in(fields.allocator),
           affected_module_idx_to_init_fn_name: &module_idx_to_init_fn_name,
           dependencies: FxIndexSet::default(),
+          imports: FxHashSet::default(),
         };
 
         finalizer.visit_program(fields.program);
diff --git a/crates/rolldown/src/runtime/runtime-extra-dev.js b/crates/rolldown/src/runtime/runtime-extra-dev.js
index 68219c825b8c..0be00ab63667 100644
--- a/crates/rolldown/src/runtime/runtime-extra-dev.js
+++ b/crates/rolldown/src/runtime/runtime-extra-dev.js
@@ -125,7 +125,9 @@ class DevRuntime {
   createEsmInitializer = (fn, res) => () => (fn && (res = fn(fn = 0)), res)
   // __commonJSMin
   createCjsInitializer = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports)
-}
+  // @ts-expect-error it exists
+  __toESM = __toESM;
+} 
 
 globalThis.__rolldown_runtime__ = DevRuntime.getInstance();
 
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 df83e263035f..573e90eaa0cc 100644
--- a/crates/rolldown/tests/snapshots/integration_rolldown__filename_with_hash.snap
+++ b/crates/rolldown/tests/snapshots/integration_rolldown__filename_with_hash.snap
@@ -4659,7 +4659,7 @@ expression: output
 
 # tests/rolldown/issues/4129
 
-- main-!~{000}~.js => main-BwmD_lXY.js
+- main-!~{000}~.js => main-DHp4Ws7A.js
 
 # tests/rolldown/issues/4196
 
@@ -5204,13 +5204,13 @@ expression: output
 
 # tests/rolldown/topics/hmr/mutiply-entires
 
-- entry-!~{000}~.js => entry-DT9qLAS4.js
-- index-!~{001}~.js => index-DXGbv_kK.js
-- chunk-!~{002}~.js => chunk-DHqyPXea.js
+- entry-!~{000}~.js => entry-D3qdtA0j.js
+- index-!~{001}~.js => index-Bbp4lvLR.js
+- chunk-!~{002}~.js => chunk-DnGosdBm.js
 
 # tests/rolldown/topics/hmr/register_exports
 
-- main-!~{000}~.js => main-C6iYd5DX.js
+- main-!~{000}~.js => main-BdEZr9rY.js
 
 # tests/rolldown/topics/import_meta_url_dirname_filename_polyfill/node_cjs
 
diff --git a/crates/rolldown_common/src/module/mod.rs b/crates/rolldown_common/src/module/mod.rs
index 36a59d70577a..78246d17eace 100644
--- a/crates/rolldown_common/src/module/mod.rs
+++ b/crates/rolldown_common/src/module/mod.rs
@@ -58,6 +58,13 @@ impl Module {
     }
   }
 
+  pub fn repr_name(&self) -> &str {
+    match self {
+      Module::Normal(v) => v.repr_name.as_str(),
+      Module::External(v) => v.identifier_name.as_str(),
+    }
+  }
+
   pub fn normal(v: NormalModule) -> Self {
     Module::Normal(Box::new(v))
   }


Reviews:
user: login: underfin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T09:36:23Z
user: login: hyf0
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-09T17:12:40Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T04:23:56Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T04:52:17Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T04:53:34Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T04:56:43Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T05:02:05Z

Review comments:
diff_hunk: @@ -125,6 +125,8 @@ class DevRuntime {
   createEsmInitializer = (fn, res) => () => (fn && (res = fn(fn = 0)), res)
   // __commonJSMin
   createCjsInitializer = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports)
+  // @ts-expect-error it is exits
+  __toESM = __toESM
path: crates/rolldown/src/runtime/runtime-extra-dev.js
user: login: underfin
body: Here just using the simply way to import runtime symbol, copy it need to copy it's dependent symbols.
created_at: 2025-05-09T09:36:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -153,6 +154,37 @@ impl<'ast> HmrAstFinalizer<'_, 'ast> {
       *expr = self.snippet.id_ref_expr(&hot_name, SPAN);
     }
   }
+
+  fn crate_binding_name(importee: &Module) -> String {
path: crates/rolldown/src/hmr/hmr_ast_finalizer.rs
user: login: graphite-app[bot]
body: There appears to be a typo in the function name: `crate_binding_name` should be `create_binding_name` to correctly reflect its purpose of creating a binding name.
```suggestion
  fn create_binding_name(importee: &Module) -> String {
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=rolldown&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-10T04:23:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -265,106 +297,88 @@ impl<'ast> VisitMut<'ast> for HmrAstFinalizer<'_, 'ast> {
           // ```
           let rec_id = self.module.imports[&import_decl.span];
           let rec = &self.module.import_records[rec_id];
-          match &self.modules[rec.resolved_module] {
-            Module::Normal(importee) => {
-              self.dependencies.insert(rec.resolved_module);
-              let id = &importee.stable_id;
-              let binding_name = format!("import_{}", importee.repr_name);
-              let stmt = quote_stmt(
-                self.alloc,
-                format!("const {binding_name} = __rolldown_runtime__.loadExports({id:?});",)
-                  .as_str(),
-              );
-              import_decl.specifiers.as_ref().inspect(|specifiers| {
-                specifiers.iter().for_each(|spec| match spec {
-                  ast::ImportDeclarationSpecifier::ImportSpecifier(import_specifier) => {
-                    self.import_binding.insert(
-                      import_specifier.local.expect_symbol_id(),
-                      format!("{binding_name}.{}", import_specifier.imported.name()),
-                    );
-                  }
-                  ast::ImportDeclarationSpecifier::ImportDefaultSpecifier(
-                    import_default_specifier,
-                  ) => {
-                    self.import_binding.insert(
-                      import_default_specifier.local.expect_symbol_id(),
-                      format!("{binding_name}.default"),
-                    );
-                  }
-                  ast::ImportDeclarationSpecifier::ImportNamespaceSpecifier(
-                    import_namespace_specifier,
-                  ) => {
-                    self.import_binding.insert(
-                      import_namespace_specifier.local.expect_symbol_id(),
-                      binding_name.to_string(),
-                    );
-                  }
-                });
-              });
-              *node = stmt;
-            }
-            Module::External(_importee) => {
-              todo!("handle external module");
-            }
+          let importee = &self.modules[rec.resolved_module];
+          self.dependencies.insert(rec.resolved_module);
+
+          let binding_name = Self::crate_binding_name(importee);
path: crates/rolldown/src/hmr/hmr_ast_finalizer.rs
user: login: graphite-app[bot]
body: There appears to be a typo in the function name. The code calls `Self::crate_binding_name(importee)` but the function is defined as `create_binding_name` on line 158. This should be corrected to `Self::create_binding_name(importee)` to match the function definition.
```suggestion
          let binding_name = Self::create_binding_name(importee);
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=rolldown&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-10T04:52:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,6 +125,8 @@ class DevRuntime {
   createEsmInitializer = (fn, res) => () => (fn && (res = fn(fn = 0)), res)
   // __commonJSMin
   createCjsInitializer = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports)
+  // @ts-expect-error it is exits
path: crates/rolldown/src/runtime/runtime-extra-dev.js
user: login: graphite-app[bot]
body: There appears to be a typo in the comment: `// @ts-expect-error it is exits` should be `// @ts-expect-error it exists`. This would more clearly communicate that the `__toESM` function exists but TypeScript doesn't recognize it.
```suggestion
+  // @ts-expect-error it exists
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=rolldown&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-10T04:53:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -153,6 +154,37 @@ impl<'ast> HmrAstFinalizer<'_, 'ast> {
       *expr = self.snippet.id_ref_expr(&hot_name, SPAN);
     }
   }
+
+  fn create_binding_name(importee: &Module) -> String {
+    format!("import_{}", importee.repr_name())
+  }
+
+  fn create_load_exports_call_stmt(
+    &mut self,
+    importee: &Module,
+    binding_name: &str,
+    span: Span,
+  ) -> Option<Statement<'ast>> {
+    if self.imports.contains(&importee.idx()) {
+      return None;
+    }
+    self.imports.insert(importee.idx());
path: crates/rolldown/src/hmr/hmr_ast_finalizer.rs
user: login: graphite-app[bot]
body: The current placement of `self.imports.insert(importee.idx())` creates a potential issue. If the function returns `None` because the module is already imported, the module index is still inserted into the set. This is logically inconsistent.

Consider moving the insert operation to just before returning `Some(stmt)`:

```rust
if self.imports.contains(&importee.idx()) {
  return None;
}

// Create the statement...

self.imports.insert(importee.idx());
Some(stmt)
```

This ensures that modules are only marked as imported when they actually generate an import statement.

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=rolldown&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-10T04:56:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -265,106 +297,88 @@ impl<'ast> VisitMut<'ast> for HmrAstFinalizer<'_, 'ast> {
           // ```
           let rec_id = self.module.imports[&import_decl.span];
           let rec = &self.module.import_records[rec_id];
-          match &self.modules[rec.resolved_module] {
-            Module::Normal(importee) => {
-              self.dependencies.insert(rec.resolved_module);
-              let id = &importee.stable_id;
-              let binding_name = format!("import_{}", importee.repr_name);
-              let stmt = quote_stmt(
-                self.alloc,
-                format!("const {binding_name} = __rolldown_runtime__.loadExports({id:?});",)
-                  .as_str(),
-              );
-              import_decl.specifiers.as_ref().inspect(|specifiers| {
-                specifiers.iter().for_each(|spec| match spec {
-                  ast::ImportDeclarationSpecifier::ImportSpecifier(import_specifier) => {
-                    self.import_binding.insert(
-                      import_specifier.local.expect_symbol_id(),
-                      format!("{binding_name}.{}", import_specifier.imported.name()),
-                    );
-                  }
-                  ast::ImportDeclarationSpecifier::ImportDefaultSpecifier(
-                    import_default_specifier,
-                  ) => {
-                    self.import_binding.insert(
-                      import_default_specifier.local.expect_symbol_id(),
-                      format!("{binding_name}.default"),
-                    );
-                  }
-                  ast::ImportDeclarationSpecifier::ImportNamespaceSpecifier(
-                    import_namespace_specifier,
-                  ) => {
-                    self.import_binding.insert(
-                      import_namespace_specifier.local.expect_symbol_id(),
-                      binding_name.to_string(),
-                    );
-                  }
-                });
-              });
-              *node = stmt;
-            }
-            Module::External(_importee) => {
-              todo!("handle external module");
-            }
+          let importee = &self.modules[rec.resolved_module];
+          self.dependencies.insert(rec.resolved_module);
+
+          let binding_name = Self::create_binding_name(importee);
+          import_decl.specifiers.as_ref().inspect(|specifiers| {
+            specifiers.iter().for_each(|spec| match spec {
+              ast::ImportDeclarationSpecifier::ImportSpecifier(import_specifier) => {
+                self.import_binding.insert(
+                  import_specifier.local.expect_symbol_id(),
+                  format!("{binding_name}.{}", import_specifier.imported.name()),
+                );
+              }
+              ast::ImportDeclarationSpecifier::ImportDefaultSpecifier(import_default_specifier) => {
+                self.import_binding.insert(
+                  import_default_specifier.local.expect_symbol_id(),
+                  format!("{binding_name}.default"),
+                );
+              }
+              ast::ImportDeclarationSpecifier::ImportNamespaceSpecifier(
+                import_namespace_specifier,
+              ) => {
+                self.import_binding.insert(
+                  import_namespace_specifier.local.expect_symbol_id(),
+                  binding_name.to_string(),
+                );
+              }
+            });
+          });
+          if let Some(stmt) =
+            self.create_load_exports_call_stmt(importee, &binding_name, import_decl.span)
+          {
+            *node = stmt;
           }
         }
         ast::ModuleDeclaration::ExportNamedDeclaration(decl) => {
           if let Some(_source) = &decl.source {
             // export {} from '...'
             let rec_id = self.module.imports[&decl.span];
             let rec = &self.module.import_records[rec_id];
+            let importee = &self.modules[rec.resolved_module];
             self.dependencies.insert(rec.resolved_module);
-            match &self.modules[rec.resolved_module] {
-              Module::Normal(importee) => {
-                self.dependencies.insert(rec.resolved_module);
-                let id = &importee.stable_id;
-                let binding_name = format!("import_{}", importee.repr_name);
-                let stmt = quote_stmt(
-                  self.alloc,
-                  format!("const {binding_name} = __rolldown_runtime__.loadExports({id:?});",)
-                    .as_str(),
-                );
-                self.exports.extend(decl.specifiers.iter().map(|specifier| {
-                  self.snippet.object_property_kind_object_property(
-                    &specifier.exported.name(),
-                    match &specifier.local {
-                      ast::ModuleExportName::IdentifierName(ident) => {
-                        Expression::StaticMemberExpression(
-                          self.snippet.builder.alloc_static_member_expression(
-                            SPAN,
-                            self.snippet.id_ref_expr(&binding_name, SPAN),
-                            self.snippet.builder.identifier_name(SPAN, ident.name.as_str()),
-                            false,
-                          ),
-                        )
-                      }
-                      ast::ModuleExportName::StringLiteral(str) => {
-                        Expression::ComputedMemberExpression(
-                          self.snippet.builder.alloc_computed_member_expression(
-                            SPAN,
-                            self.snippet.id_ref_expr(&binding_name, SPAN),
-                            self.snippet.builder.expression_string_literal(
-                              SPAN, str.value.as_str(), None
-                            ),
-                            false,
-                          ),
-                        )
-                      }
-                      ast::ModuleExportName::IdentifierReference(_) => {
-                        unreachable!(
-                          "ModuleExportName IdentifierReference is invalid in ExportNamedDeclaration with source"
-                        )
-                      }
-                    },
-                    matches!(specifier.exported, ast::ModuleExportName::StringLiteral(_))
-                  )
-                }));
 
-                *node = stmt;
-              }
-              Module::External(_importee) => {
-                todo!("handle external module");
-              }
+            let binding_name = Self::create_binding_name(importee);
+            self.exports.extend(decl.specifiers.iter().map(|specifier| {
+              self.snippet.object_property_kind_object_property(
+                &specifier.exported.name(),
+                match &specifier.local {
+                  ast::ModuleExportName::IdentifierName(ident) => {
+                    Expression::StaticMemberExpression(
+                      self.snippet.builder.alloc_static_member_expression(
+                        SPAN,
+                        self.snippet.id_ref_expr(&binding_name, SPAN),
+                        self.snippet.builder.identifier_name(SPAN, ident.name.as_str()),
+                        false,
+                      ),
+                    )
+                  }
+                  ast::ModuleExportName::StringLiteral(str) => {
+                    Expression::ComputedMemberExpression(
+                      self.snippet.builder.alloc_computed_member_expression(
+                        SPAN,
+                        self.snippet.id_ref_expr(&binding_name, SPAN),
+                        self.snippet.builder.expression_string_literal(
+                          SPAN, str.value.as_str(), None
+                        ),
+                        false,
+                      ),
+                    )
+                  }
+                  ast::ModuleExportName::IdentifierReference(_) => {
+                    unreachable!(
+                      "ModuleExportName IdentifierReference is invalid in ExportNamedDeclaration with source"
+                    )
+                  }
+                },
+                matches!(specifier.exported, ast::ModuleExportName::StringLiteral(_))
+              )
+            }));
+            if let Some(stmt) =
+              self.create_load_exports_call_stmt(importee, &binding_name, decl.span)
+            {
+              *node = stmt;
path: crates/rolldown/src/hmr/hmr_ast_finalizer.rs
user: login: graphite-app[bot]
body: When `create_load_exports_call_stmt` returns `None` (indicating the module has already been imported), the original export declaration node remains unchanged. This could result in invalid code generation since the export declaration would still reference the source module but wouldn't be properly transformed.

Consider handling this case explicitly by replacing the node with an empty statement or removing it when no new import statement is needed:

```rust
if let Some(stmt) = self.create_load_exports_call_stmt(importee, &binding_name, decl.span) {
  *node = stmt;
} else {
  // Replace with empty statement or other appropriate handling when the module is already imported
  *node = self.snippet.empty_stmt(decl.span);
}
```

This ensures the export declaration is properly handled in all cases.
```suggestion

            if let Some(stmt) =
              self.create_load_exports_call_stmt(importee, &binding_name, decl.span)
            {
              *node = stmt;
            } else {
              // Replace with empty statement when the module is already imported
              *node = self.snippet.empty_stmt(decl.span);
            }

```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=rolldown&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-10T05:02:04Z
author_association: CONTRIBUTOR

PR comments:
user: login: underfin
created_at: 2025-05-09T09:32:06Z
body: * **#4461** <a href="https://app.graphite.dev/github/pr/rolldown/rolldown/4461?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#4460** <a href="https://app.graphite.dev/github/pr/rolldown/rolldown/4460?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/4460?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: CONTRIBUTOR
user: login: netlify[bot]
created_at: 2025-05-09T09:32:21Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *rolldown-rs* canceled.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | a3ee2ad94d519fcc43f25c95f87e51240155ffda |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/rolldown-rs/deploys/681edd943bd80e0008e66abe |
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-09T09:53:30Z
body: ### Benchmarks Rust
- target: `main`(093c41d8cb7e31a6ef191c5cdd5ba08de3173ed0)
- pr: `05-08-refactor_hmr_hmr_chunk_loadexports`(a3ee2ad94d519fcc43f25c95f87e51240155ffda)
```
group                                                               pr                                     target
-----                                                               --                                     ------
bundle/bundle@multi-duplicated-top-level-symbol                     1.00     79.2±1.08ms        ? ?/sec    1.00     79.1±1.65ms        ? ?/sec
bundle/bundle@multi-duplicated-top-level-symbol-minify              1.00     99.5±0.96ms        ? ?/sec    1.04    103.0±1.72ms        ? ?/sec
bundle/bundle@multi-duplicated-top-level-symbol-minify-sourcemap    1.00    113.1±1.22ms        ? ?/sec    1.04    117.7±2.30ms        ? ?/sec
bundle/bundle@multi-duplicated-top-level-symbol-sourcemap           1.00     88.7±1.27ms        ? ?/sec    1.00     88.8±1.03ms        ? ?/sec
bundle/bundle@rome-ts                                               1.00    121.0±1.28ms        ? ?/sec    1.02    123.8±1.31ms        ? ?/sec
bundle/bundle@rome-ts-minify                                        1.00    197.6±6.77ms        ? ?/sec    1.02    201.6±2.11ms        ? ?/sec
bundle/bundle@rome-ts-minify-sourcemap                              1.02    237.0±2.56ms        ? ?/sec    1.00    232.1±1.61ms        ? ?/sec
bundle/bundle@rome-ts-sourcemap                                     1.00    134.1±2.35ms        ? ?/sec    1.03    138.2±1.95ms        ? ?/sec
bundle/bundle@threejs                                               1.00     40.4±0.99ms        ? ?/sec    1.06     42.7±1.20ms        ? ?/sec
bundle/bundle@threejs-minify                                        1.00     82.8±0.66ms        ? ?/sec    1.05     86.9±4.39ms        ? ?/sec
bundle/bundle@threejs-minify-sourcemap                              1.00    102.1±0.71ms        ? ?/sec    1.04    105.9±1.07ms        ? ?/sec
bundle/bundle@threejs-sourcemap                                     1.00     46.8±0.42ms        ? ?/sec    1.07     50.0±0.43ms        ? ?/sec
bundle/bundle@threejs10x                                            1.00    418.0±3.94ms        ? ?/sec    1.03    432.4±4.19ms        ? ?/sec
bundle/bundle@threejs10x-minify                                     1.00   1031.4±6.15ms        ? ?/sec    1.02   1050.9±3.78ms        ? ?/sec
bundle/bundle@threejs10x-minify-sourcemap                           1.00   1223.8±4.48ms        ? ?/sec    1.00   1228.9±4.49ms        ? ?/sec
bundle/bundle@threejs10x-sourcemap                                  1.00   495.9±17.41ms        ? ?/sec    1.02    505.0±2.97ms        ? ?/sec
remapping/remapping                                                 1.00     24.2±2.44ms        ? ?/sec    1.16     28.0±0.42ms        ? ?/sec
remapping/render-chunk-remapping                                    1.05     72.2±6.56ms        ? ?/sec    1.00     69.1±4.49ms        ? ?/sec
scan/scan@rome-ts                                                   1.00     93.8±0.87ms        ? ?/sec    1.00     94.1±1.67ms        ? ?/sec
scan/scan@threejs                                                   1.01     31.0±0.32ms        ? ?/sec    1.00     30.8±1.02ms        ? ?/sec
scan/scan@threejs10x                                                1.03    318.3±2.59ms        ? ?/sec    1.00    310.5±2.61ms        ? ?/sec
```
author_association: CONTRIBUTOR