Repository: vercel/next.js
Pull request: Turbopack: prevent ParseResult recomputation

There was a mistake here: `process_parse_result` consumes the `ParseResult` via `ReadRef::try_unwrap`, but then inside of that, `EsmExports ::code_gen` does another read of the same cell with `parse_result.await?`. That required a recomputation because the cell was destroyed shortly before.
Don't do that anymore.

Now, this is the only remaining recomputation, but that is somewhat expected (multiple facade modules have the same empty `ParseResult`):

![Bildschirmfoto 2025-07-03 um 16 18 15](https://github.com/user-attachments/assets/468e1ef6-3e5a-4508-be48-da037bd44263)


Closes PACK-5004

Changed files:
filename: turbopack/crates/turbopack-ecmascript/src/lib.rs
patch: @@ -870,9 +870,11 @@ impl EcmascriptModuleContentOptions {
     async fn merged_code_gens(
         &self,
         scope_hoisting_context: ScopeHoistingContext<'_>,
+        eval_context: &EvalContext,
     ) -> Result<Vec<CodeGeneration>> {
+        // Don't read `parsed` here again, it will cause a recomputation as `process_parse_result`
+        // has consumed the cell already.
         let EcmascriptModuleContentOptions {
-            parsed,
             module,
             chunking_context,
             references,
@@ -906,7 +908,7 @@ impl EcmascriptModuleContentOptions {
                             .code_generation(
                                 **chunking_context,
                                 scope_hoisting_context,
-                                Some(**parsed),
+                                eval_context,
                                 *module,
                             )
                             .await?,
@@ -1096,7 +1098,6 @@ impl EcmascriptModuleContent {
                     modules_header_width,
                     comments,
                 },
-                export_contexts: None,
                 is_esm: true,
                 strict: true,
                 original_source_map: CodeGenResultOriginalSourceMap::ScopeHoisting(
@@ -1274,8 +1275,9 @@ async fn merge_modules(
             Ok((
                 *module,
                 content
-                    .export_contexts
+                    .scope_hoisting_syntax_contexts
                     .as_ref()
+                    .map(|(_, export_contexts)| export_contexts)
                     .context("expected exports contexts")?,
             ))
         })
@@ -1294,7 +1296,7 @@ async fn merge_modules(
              program: &mut Program| {
                 let _ = tracing::trace_span!("prepare module").entered();
                 if let CodeGenResult {
-                    scope_hoisting_syntax_contexts: Some(module_contexts),
+                    scope_hoisting_syntax_contexts: Some((module_contexts, _)),
                     ..
                 } = content
                 {
@@ -1555,14 +1557,16 @@ struct CodeGenResult {
     program: Program,
     source_map: CodeGenResultSourceMap,
     comments: CodeGenResultComments,
-    /// `eval_context.imports.exports`
-    export_contexts: Option<FxHashMap<RcStr, Id>>,
     is_esm: bool,
     strict: bool,
     original_source_map: CodeGenResultOriginalSourceMap,
     minify: MinifyType,
-    scope_hoisting_syntax_contexts:
-        Option<FxDashMap<ResolvedVc<Box<dyn EcmascriptChunkPlaceable + 'static>>, SyntaxContext>>,
+    #[allow(clippy::type_complexity)]
+    /// (Map<Module, corresponding context for imports>, `eval_context.imports.exports`)
+    scope_hoisting_syntax_contexts: Option<(
+        FxDashMap<ResolvedVc<Box<dyn EcmascriptChunkPlaceable + 'static>>, SyntaxContext>,
+        FxHashMap<RcStr, Id>,
+    )>,
 }
 
 struct ScopeHoistingOptions<'a> {
@@ -1584,22 +1588,21 @@ async fn process_parse_result(
     with_consumed_parse_result(
         parsed,
         async |mut program, source_map, globals, eval_context, comments| -> Result<CodeGenResult> {
-            let (top_level_mark, is_esm, strict, export_contexts) = eval_context
+            let (top_level_mark, is_esm, strict) = eval_context
+                .as_ref()
                 .map_either(
                     |e| {
                         (
                             e.top_level_mark,
                             e.is_esm(specified_module_type),
                             e.imports.strict,
-                            Cow::Owned(e.imports.exports),
                         )
                     },
                     |e| {
                         (
                             e.top_level_mark,
                             e.is_esm(specified_module_type),
                             e.imports.strict,
-                            Cow::Borrowed(&e.imports.exports),
                         )
                     },
                 )
@@ -1617,7 +1620,23 @@ async fn process_parse_result(
                         is_import_mark,
                         globals,
                     };
-                    let code_gens = options.unwrap().merged_code_gens(ctx).await?;
+                    let code_gens = options
+                        .unwrap()
+                        .merged_code_gens(
+                            ctx,
+                            match &eval_context {
+                                Either::Left(e) => e,
+                                Either::Right(e) => e,
+                            },
+                        )
+                        .await?;
+
+                    let export_contexts = eval_context
+                        .map_either(
+                            |e| Cow::Owned(e.imports.exports),
+                            |e| Cow::Borrowed(&e.imports.exports),
+                        )
+                        .into_inner();
                     let preserved_exports =
                         match &*scope_hoisting_options.module.get_exports().await? {
                             EcmascriptExports::EsmExports(exports) => exports
@@ -1652,12 +1671,21 @@ async fn process_parse_result(
                             is_import_mark,
                             module_syntax_contexts_cache,
                             preserved_exports,
+                            export_contexts,
                         )),
                         prepend_ident_comment,
                     )
                 } else if let Some(options) = options {
                     (
-                        options.merged_code_gens(ScopeHoistingContext::None).await?,
+                        options
+                            .merged_code_gens(
+                                ScopeHoistingContext::None,
+                                match &eval_context {
+                                    Either::Left(e) => e,
+                                    Either::Right(e) => e,
+                                },
+                            )
+                            .await?,
                         None,
                         None,
                     )
@@ -1696,7 +1724,7 @@ async fn process_parse_result(
                     }
                 }
 
-                if let Some((is_import_mark, _, preserved_exports)) = &retain_syntax_context {
+                if let Some((is_import_mark, _, preserved_exports, _)) = &retain_syntax_context {
                     program.visit_mut_with(&mut hygiene_rename_only(
                         Some(top_level_mark),
                         *is_import_mark,
@@ -1733,13 +1761,13 @@ async fn process_parse_result(
                     comments,
                     extra_comments,
                 },
-                // TODO ideally don't clone here at all
-                export_contexts: Some(export_contexts.into_owned()),
                 is_esm,
                 strict,
                 original_source_map: CodeGenResultOriginalSourceMap::Single(original_source_map),
                 minify,
-                scope_hoisting_syntax_contexts: retain_syntax_context.map(|(_, ctxts, _)| ctxts),
+                scope_hoisting_syntax_contexts: retain_syntax_context
+                    // TODO ideally don't clone here
+                    .map(|(_, ctxts, _, export_contexts)| (ctxts, export_contexts.into_owned())),
             })
         },
         async |parse_result| -> Result<CodeGenResult> {
@@ -1769,7 +1797,6 @@ async fn process_parse_result(
                         }),
                         source_map: CodeGenResultSourceMap::None,
                         comments: CodeGenResultComments::Empty,
-                        export_contexts: None,
                         is_esm: false,
                         strict: false,
                         original_source_map: CodeGenResultOriginalSourceMap::Single(None),
@@ -1796,7 +1823,6 @@ async fn process_parse_result(
                         }),
                         source_map: CodeGenResultSourceMap::None,
                         comments: CodeGenResultComments::Empty,
-                        export_contexts: None,
                         is_esm: false,
                         strict: false,
                         original_source_map: CodeGenResultOriginalSourceMap::Single(None),
@@ -1891,7 +1917,6 @@ async fn emit_content(
         strict,
         original_source_map,
         minify,
-        export_contexts: _,
         scope_hoisting_syntax_contexts: _,
     } = content;
 

filename: turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs
patch: @@ -30,10 +30,10 @@ use turbopack_core::{
 use super::base::ReferencedAsset;
 use crate::{
     EcmascriptModuleAsset, ScopeHoistingContext,
+    analyzer::graph::EvalContext,
     chunk::{EcmascriptChunkPlaceable, EcmascriptExports},
     code_gen::{CodeGeneration, CodeGenerationHoistedStmt},
     magic_identifier,
-    parse::ParseResult,
     runtime_functions::{TURBOPACK_DYNAMIC, TURBOPACK_ESM},
     tree_shake::asset::EcmascriptModulePartAsset,
     utils::module_id_to_lit,
@@ -555,7 +555,7 @@ impl EsmExports {
         self: Vc<Self>,
         chunking_context: Vc<Box<dyn ChunkingContext>>,
         scope_hoisting_context: ScopeHoistingContext<'_>,
-        parsed: Option<Vc<ParseResult>>,
+        eval_context: &EvalContext,
         module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
     ) -> Result<CodeGeneration> {
         let export_usage_info = chunking_context.module_export_usage(*ResolvedVc::upcast(module));
@@ -570,12 +570,6 @@ impl EsmExports {
             return Ok(CodeGeneration::empty());
         }
 
-        let parsed = if let Some(parsed) = parsed {
-            Some(parsed.await?)
-        } else {
-            None
-        };
-
         let mut dynamic_exports = Vec::<Box<Expr>>::new();
         {
             let id = if let Some(module) = scope_hoisting_context.module()
@@ -620,24 +614,16 @@ impl EsmExports {
                     // 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,
-                                )
-                            }
+                    let binding =
+                        if let Some((local, ctxt)) = eval_context.imports.exports.get(exported) {
+                            Some((Cow::Borrowed(local.as_str()), *ctxt))
                         } else {
-                            None
-                        }
-                    } else {
-                        None
-                    };
+                            bail!(
+                                "Expected export to be in eval context {:?} {:?}",
+                                exported,
+                                eval_context.imports,
+                            )
+                        };
                     let (local, ctxt) = binding.unwrap_or_else(|| {
                         // Fallback, shouldn't happen in practice
                         (