Repository: vercel/next.js
Pull request: Turbopack: fix double codegen of some merged modules

This fixes double-codegen and re-parse (due to recomputation).

Previously, `get_batch_group` could return some of the original JS modules, while `chunkable_items` was correctly getting the replaced `MergedEcmascriptModule`s. This cause modules to be codegened twice.
Instead, replace `ModuleBatch`es in both of the lists.

Before:
<img width="1069" height="975" alt="Bildschirmfoto 2025-09-17 um 11 59 21" src="https://github.com/user-attachments/assets/74e0258b-9651-4e6d-b433-f2d81f0b96ce" />


After:
<img width="1591" height="1044" alt="Bildschirmfoto 2025-09-17 um 12 15 24" src="https://github.com/user-attachments/assets/170b36ca-5b64-4b0f-851e-06305baf86a7" />


Closes PACK-5483

Changed files:
filename: turbopack/crates/turbopack-core/src/chunk/chunk_group.rs
patch: @@ -1,4 +1,4 @@
-use std::{cell::RefCell, collections::HashSet};
+use std::{cell::RefCell, collections::HashSet, sync::atomic::AtomicBool};
 
 use anyhow::{Context, Result};
 use rustc_hash::FxHashMap;
@@ -20,7 +20,8 @@ use crate::{
         GraphTraversalAction, ModuleGraph,
         merged_modules::MergedModuleInfo,
         module_batch::{
-            ChunkableModuleBatchGroup, ChunkableModuleOrBatch, ModuleBatch, ModuleOrBatch,
+            ChunkableModuleBatchGroup, ChunkableModuleOrBatch, ModuleBatch, ModuleBatchGroup,
+            ModuleOrBatch,
         },
         module_batches::{BatchingConfig, ModuleBatchesGraphEdge},
     },
@@ -373,6 +374,16 @@ pub async fn chunk_group_content(
         }
     }
 
+    let batch_groups = if let Some((merged_modules, _)) = &should_merge_modules {
+        batch_groups
+            .into_iter()
+            .map(|group| map_module_batch_group(*merged_modules, *group).to_resolved())
+            .try_join()
+            .await?
+    } else {
+        batch_groups.into_iter().collect()
+    };
+
     Ok(ChunkGroupContent {
         chunkable_items,
         batch_groups,
@@ -422,3 +433,54 @@ async fn map_module_batch(
         Ok(batch)
     }
 }
+
+#[turbo_tasks::function]
+async fn map_module_batch_group(
+    merged_modules: Vc<MergedModuleInfo>,
+    group: Vc<ModuleBatchGroup>,
+) -> Result<Vc<ModuleBatchGroup>> {
+    let merged_modules_ref = merged_modules.await?;
+    let group_ref = group.await?;
+
+    let modified = AtomicBool::new(false);
+    let items = group_ref
+        .items
+        .iter()
+        .copied()
+        .map(async |chunkable_module| match chunkable_module {
+            ModuleOrBatch::Module(module) => {
+                if !merged_modules_ref.should_create_chunk_item_for(module) {
+                    modified.store(true, std::sync::atomic::Ordering::Relaxed);
+                    return Ok(None);
+                }
+
+                let module =
+                    if let Some(replacement) = merged_modules_ref.should_replace_module(module) {
+                        modified.store(true, std::sync::atomic::Ordering::Relaxed);
+                        ResolvedVc::upcast(replacement)
+                    } else {
+                        module
+                    };
+
+                Ok(Some(ModuleOrBatch::Module(module)))
+            }
+            ModuleOrBatch::Batch(batch) => {
+                let replacement = map_module_batch(merged_modules, *batch)
+                    .to_resolved()
+                    .await?;
+                if replacement != batch {
+                    modified.store(true, std::sync::atomic::Ordering::Relaxed);
+                }
+                Ok(Some(ModuleOrBatch::Batch(replacement)))
+            }
+            ModuleOrBatch::None(i) => Ok(Some(ModuleOrBatch::None(i))),
+        })
+        .try_flat_join()
+        .await?;
+
+    if modified.into_inner() {
+        Ok(ModuleBatchGroup::new(items, group_ref.chunk_groups.clone()))
+    } else {
+        Ok(group)
+    }
+}

filename: turbopack/crates/turbopack-core/src/chunk/mod.rs
patch: @@ -428,7 +428,7 @@ pub trait ChunkableModuleReference: ModuleReference + ValueToString {
 
 pub struct ChunkGroupContent {
     pub chunkable_items: Vec<ChunkableModuleOrBatch>,
-    pub batch_groups: FxIndexSet<ResolvedVc<ModuleBatchGroup>>,
+    pub batch_groups: Vec<ResolvedVc<ModuleBatchGroup>>,
     pub async_modules: FxIndexSet<ResolvedVc<Box<dyn ChunkableModule>>>,
     pub traced_modules: FxIndexSet<ResolvedVc<Box<dyn Module>>>,
     pub availability_info: AvailabilityInfo,