Repository: vercel/next.js
Pull request: Turbopack: fix export collection

This was slightly broken, as I noticed while debugging scope hoisting

Closes PACK-4786

Changed files:
filename: turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs
patch: @@ -3,7 +3,7 @@ use std::{collections::BTreeMap, fmt::Display};
 use once_cell::sync::Lazy;
 use rustc_hash::{FxHashMap, FxHashSet};
 use swc_core::{
-    common::{BytePos, Span, Spanned, comments::Comments, source_map::SmallPos},
+    common::{BytePos, Span, Spanned, SyntaxContext, comments::Comments, source_map::SmallPos},
     ecma::{
         ast::*,
         atoms::{Atom, atom},
@@ -19,6 +19,7 @@ use super::{JsValue, ModuleValue, top_level_await::has_top_level_await};
 use crate::{
     SpecifiedModuleType,
     analyzer::{ConstantValue, ObjectPart},
+    magic_identifier,
     tree_shake::{PartId, find_turbopack_part_id_in_asserts},
 };
 
@@ -447,10 +448,10 @@ impl Analyzer<'_> {
     }
 }
 
-fn to_word(name: &ModuleExportName) -> Atom {
+fn export_as_atom(name: &ModuleExportName) -> &Atom {
     match name {
-        ModuleExportName::Ident(ident) => ident.sym.clone(),
-        ModuleExportName::Str(str) => str.value.clone(),
+        ModuleExportName::Ident(ident) => &ident.sym,
+        ModuleExportName::Str(s) => &s.value,
     }
 }
 
@@ -542,6 +543,7 @@ impl Visit for Analyzer<'_> {
         self.data.has_exports = true;
 
         let Some(ref src) = export.src else {
+            export.visit_children_with(self);
             return;
         };
 
@@ -575,7 +577,7 @@ impl Visit for Analyzer<'_> {
                     self.data.reexports.push((
                         i,
                         Reexport::Namespace {
-                            exported: to_word(&n.name),
+                            exported: export_as_atom(&n.name).clone(),
                         },
                     ));
                 }
@@ -592,8 +594,9 @@ impl Visit for Analyzer<'_> {
                     self.data.reexports.push((
                         i,
                         Reexport::Named {
-                            imported: to_word(&n.orig),
-                            exported: to_word(n.exported.as_ref().unwrap_or(&n.orig)),
+                            imported: export_as_atom(&n.orig).clone(),
+                            exported: export_as_atom(n.exported.as_ref().unwrap_or(&n.orig))
+                                .clone(),
                         },
                     ));
                 }
@@ -637,6 +640,15 @@ impl Visit for Analyzer<'_> {
             // only visit children if we potentially need to mark import / requires
             n.visit_children_with(self);
         }
+
+        self.data.exports.insert(
+            rcstr!("default"),
+            (
+                // `EsmModuleItem::code_generation` inserts this variable.
+                magic_identifier::mangle("default export").into(),
+                SyntaxContext::empty(),
+            ),
+        );
     }
     fn visit_export_default_expr(&mut self, n: &ExportDefaultExpr) {
         self.data.has_exports = true;
@@ -645,14 +657,27 @@ impl Visit for Analyzer<'_> {
             // only visit children if we potentially need to mark import / requires
             n.visit_children_with(self);
         }
+
+        self.data.exports.insert(
+            rcstr!("default"),
+            (
+                // `EsmModuleItem::code_generation` inserts this variable.
+                magic_identifier::mangle("default export").into(),
+                SyntaxContext::empty(),
+            ),
+        );
     }
 
     fn visit_export_named_specifier(&mut self, n: &ExportNamedSpecifier) {
-        if let ModuleExportName::Ident(ident) = &n.exported.as_ref().unwrap_or(&n.orig) {
-            self.data
-                .exports
-                .insert(ident.sym.as_str().into(), ident.to_id());
-        }
+        let ModuleExportName::Ident(local) = &n.orig else {
+            // This is only possible for re-exports, but they are already handled earlier in
+            // visit_named_export.
+            unreachable!("string reexports should have been already handled in visit_named_export");
+        };
+        let exported = n.exported.as_ref().unwrap_or(&n.orig);
+        self.data
+            .exports
+            .insert(export_as_atom(exported).as_str().into(), local.to_id());
     }
 
     fn visit_export_default_specifier(&mut self, n: &ExportDefaultSpecifier) {
@@ -661,14 +686,6 @@ impl Visit for Analyzer<'_> {
             .insert(rcstr!("default"), n.exported.to_id());
     }
 
-    fn visit_export_namespace_specifier(&mut self, n: &ExportNamespaceSpecifier) {
-        if let ModuleExportName::Ident(ident) = &n.name {
-            self.data
-                .exports
-                .insert(ident.sym.as_str().into(), ident.to_id());
-        }
-    }
-
     fn visit_program(&mut self, m: &Program) {
         self.data.has_top_level_await = has_top_level_await(m).is_some();
 

filename: turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs
patch: @@ -1,10 +1,10 @@
 use std::{borrow::Cow, collections::BTreeMap, ops::ControlFlow};
 
-use anyhow::Result;
+use anyhow::{Result, bail};
 use rustc_hash::FxHashSet;
 use serde::{Deserialize, Serialize};
 use swc_core::{
-    common::DUMMY_SP,
+    common::{DUMMY_SP, SyntaxContext},
     ecma::ast::{
         AssignTarget, ComputedPropName, Expr, ExprStmt, Ident, KeyValueProp, Lit, MemberExpr,
         MemberProp, ObjectLit, Prop, PropName, PropOrSpread, SimpleAssignTarget, Stmt, Str,
@@ -565,21 +565,38 @@ impl EsmExports {
                     "(() => { throw new Error(\"Failed binding. See build errors!\"); })" as Expr,
                 )),
                 EsmExport::LocalBinding(name, mutable) => {
-                    let local = if name == "default" {
-                        Cow::Owned(magic_identifier::mangle("default export"))
+                    // TODO ideally, this information would just be stored in
+                    // EsmExport::LocalBinding and we wouldn't have to re-correlated this
+                    // information with eval_context.imports.exports to get the syntax context.
+                    let binding = if let Some(parsed) = &parsed {
+                        if let ParseResult::Ok { eval_context, .. } = &**parsed {
+                            if let Some((local, ctxt)) = eval_context.imports.exports.get(exported)
+                            {
+                                Some((Cow::Borrowed(local.as_str()), *ctxt))
+                            } else {
+                                bail!(
+                                    "Expected export to be in eval context {:?} {:?}",
+                                    exported,
+                                    eval_context.imports,
+                                )
+                            }
+                        } else {
+                            None
+                        }
                     } else {
-                        Cow::Borrowed(name.as_str())
+                        None
                     };
-                    let ctxt = parsed
-                        .as_ref()
-                        .and_then(|parsed| {
-                            if let ParseResult::Ok { eval_context, .. } = &**parsed {
-                                eval_context.imports.exports.get(name).map(|id| id.1)
+                    let (local, ctxt) = binding.unwrap_or_else(|| {
+                        // Fallback, shouldn't happen in practice
+                        (
+                            if name == "default" {
+                                Cow::Owned(magic_identifier::mangle("default export"))
                             } else {
-                                None
-                            }
-                        })
-                        .unwrap_or_default();
+                                Cow::Borrowed(name.as_str())
+                            },
+                            SyntaxContext::empty(),
+                        )
+                    });
 
                     if *mutable {
                         Some(quote!(