vercel/next.js · #98734

Turbopack: consume graph-derived chunk groups

sokra · merged Oct 24, 202611 files · 506 + / 152 −
crates/next-api/src/app.rs53 + / 34 −
@@ -1,4 +1,4 @@-use anyhow::{Context, Result};+use anyhow::{Context, Result, ensure}; use bincode::{Decode, Encode}; use next_core::{     app_structure::{@@ -1967,46 +1967,65 @@ impl AppEndpoint {                     // `per_page_module_graph`), so without this the layout segments would share                     // very little across pages.                     //-                    // It must not be created in production. Its `parent` is *this* endpoint's-                    // entry chunk group, and `parent` is part of a merged group's identity, so-                    // the group -- and hence the availability info it seeds into the layout-                    // segment chain below -- would differ for every endpoint. That makes each-                    // shared layout segment re-chunk once per descendant endpoint. In production-                    // a single whole-app module graph already shares these modules, so they are-                    // chunked as part of the entry instead.+                    // It must not be created in production. The group is the merged child of+                    // *this* endpoint's entry chunk group, and that parent is part of a merged+                    // group's identity, so the group -- and hence the availability info it seeds+                    // into the layout segment chain below -- would differ for every endpoint.+                    // That makes each shared layout segment re-chunk once per descendant+                    // endpoint. In production a single whole-app module graph already shares+                    // these modules, so they are chunked as part of the entry instead.                     if *project.per_page_module_graph().await? {                         let chunk_group_info = module_graph.chunk_group_info();                         let span = tracing::trace_span!("server utils");                         async {-                            let parent_chunk_group = *chunk_group_info-                                .get_index_of(entry_chunk_group.key())-                                .await?;+                            let has_server_utils = !client_references.server_utils.is_empty();++                            // The server utilities of this entry are merged into a single group:+                            // the one merged child of the entry chunk group carrying the server+                            // utility merge tag.+                            let derived_group = if !has_server_utils {+                                None+                            } else {+                                let entry_index = *chunk_group_info+                                    .get_index_of(entry_chunk_group.key())+                                    .await?;+                                let merged = chunk_group_info+                                    .get_shared_merged_chunk_group(+                                        entry_index,+                                        NEXT_SERVER_UTILITY_MERGE_TAG,+                                    )+                                    .await?;+                                merged.as_ref().map(|group| group.chunk_group.clone())+                            };++                            ensure!(+                                !has_server_utils || derived_group.is_some(),+                                "could not find a graph-derived shared merged group for server \+                                 utilities"+                            ); -                            let server_utils = client_references-                                .server_utils-                                .iter()-                                .map(async |m| Ok(ResolvedVc::upcast(m.await?.module)))-                                .try_join()-                                .await?;-                            let chunk_group = chunking_context-                                .chunk_group(-                                    AssetIdent::from_path(-                                        this.app_project.project().project_path().owned().await?,+                            let mut combined_chunk_group = ChunkGroupResult::empty_resolved();+                            if let Some(group) = derived_group {+                                combined_chunk_group = chunking_context+                                    .chunk_group(+                                        AssetIdent::from_path(+                                            this.app_project+                                                .project()+                                                .project_path()+                                                .owned()+                                                .await?,+                                        )+                                        .with_modifier(rcstr!("server-utils"))+                                        .into_vc(),+                                        smallvec![group],+                                        module_graph,+                                        AvailabilityInfo::root(),                                     )-                                    .with_modifier(rcstr!("server-utils"))-                                    .into_vc(),-                                    smallvec![ChunkGroup::SharedMerged {-                                        merge_tag: NEXT_SERVER_UTILITY_MERGE_TAG.clone(),-                                        entries: server_utils,-                                        parent: parent_chunk_group,-                                    }],-                                    module_graph,-                                    AvailabilityInfo::root(),-                                )-                                .to_resolved()-                                .await?;+                                    .to_resolved()+                                    .await?;+                            } -                            current_chunk_group = chunk_group;+                            current_chunk_group = combined_chunk_group;                              anyhow::Ok(())                         }
crates/next-api/src/module_graph.rs2 + / 0 −
@@ -612,6 +612,7 @@ impl ClientReferencesGraph {                      // Client reference used by the template, not a server component                     client_references.push(ClientReference {+                        parent_module: node,                         server_component: None,                         ty,                     });@@ -665,6 +666,7 @@ impl ClientReferencesGraph {                         };                          client_references.push(ClientReference {+                            parent_module: module,                             server_component: Some(sc),                             ty,                         });
crates/next-core/src/next_app/app_client_references_chunks.rs304 + / 97 −
@@ -1,31 +1,170 @@-use anyhow::Result;-use smallvec::smallvec;+use anyhow::{Result, bail};+use smallvec::SmallVec; use tracing::Instrument;-use turbo_rcstr::rcstr;+use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{-    FxIndexMap, FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToStringRef, Vc,+    FxIndexMap, FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToString,+    ValueToStringRef, Vc, }; use turbopack_core::{     chunk::{ChunkGroupResult, ChunkingContext, availability_info::AvailabilityInfo},     module::Module,     module_graph::{         ModuleGraph,-        chunk_group_info::{ChunkGroup, ChunkGroupKey},+        chunk_group_info::{ChunkGroup, ChunkGroupInfo},     },     output::{OutputAsset, OutputAssets, OutputAssetsWithReferenced}, };  use crate::{     next_client_reference::{-        ClientReferenceType,-        ecmascript_client_reference::ecmascript_client_reference_module::{-            ecmascript_client_reference_merge_tag, ecmascript_client_reference_merge_tag_ssr,-        },+        CLIENT_MERGE_TAG, ClientReference, ClientReferenceType, SSR_MERGE_TAG,         visit_client_reference::ClientReferenceGraphResult,     },     next_server_component::server_component_module::NextServerComponentModule, }; +fn is_isolated_merged_group_for_parent(+    chunk_group: &ChunkGroup,+    parent_indices: &FxIndexSet<u32>,+    merge_tag: &RcStr,+) -> bool {+    matches!(+        chunk_group,+        ChunkGroup::IsolatedMerged {+            parent,+            merge_tag: group_merge_tag,+            ..+        } if group_merge_tag == merge_tag && parent_indices.contains(&(*parent as u32))+    )+}++/// The chunk groups a client reference's merged group may hang off.+///+/// A client reference is reached from its source module, which can sit in several chunk groups+/// when the reference is shared between routes. When the reference belongs to a server component,+/// the relevant groups are the ones that contain that server component too.+///+/// An empty intersection is not an error: in a development module graph the source module and the+/// server component do not necessarily share a chunk group, and framework-level references have no+/// server component at all. Falling back to the source module's own groups is the conservative+/// choice -- the reference is still chunked, just not narrowed to the component's groups.+fn relevant_parent_indices(+    source_indices: FxIndexSet<u32>,+    server_component_indices: Option<&FxIndexSet<u32>>,+) -> FxIndexSet<u32> {+    let Some(server_component_indices) = server_component_indices else {+        return source_indices;+    };+    let intersection = source_indices+        .intersection(server_component_indices)+        .copied()+        .collect::<FxIndexSet<_>>();+    if intersection.is_empty() {+        source_indices+    } else {+        intersection+    }+}++/// A client reference, the module it is reached from, and the server component that owns it (if+/// any).+#[derive(Clone, Copy)]+struct ClientReferenceWithContext {+    /// The module that is chunked for this client reference.+    entry_module: ResolvedVc<Box<dyn Module>>,+    /// The module the client reference is reached from.+    source_module: ResolvedVc<Box<dyn Module>>,+    /// The server component this client reference belongs to, if it belongs to one. Framework+    /// references do not.+    server_component: Option<ResolvedVc<Box<dyn Module>>>,+}++/// Returns the graph-derived isolated merged groups (tagged `merge_tag`) that chunk `references`,+/// with their entries as derived by the module graph.+async fn derived_isolated_merged_groups(+    chunk_group_info: Vc<ChunkGroupInfo>,+    references: &[ClientReferenceWithContext],+    merge_tag: &RcStr,+) -> Result<SmallVec<[ChunkGroup; 1]>> {+    let mut groups = FxIndexMap::default();++    for &ClientReferenceWithContext {+        entry_module,+        source_module,+        server_component,+    } in references+    {+        let source_groups = chunk_group_info+            .get_chunk_groups_for_module(*source_module)+            .await?;+        let server_component_indices = if let Some(server_component) = server_component {+            Some(+                chunk_group_info+                    .get_chunk_groups_for_module(*server_component)+                    .await?+                    .iter()+                    .map(|group| group.index)+                    .collect::<FxIndexSet<_>>(),+            )+        } else {+            None+        };+        let source_indices = source_groups.iter().map(|group| group.index).collect();+        let parent_indices =+            relevant_parent_indices(source_indices, server_component_indices.as_ref());++        let mut found = false;+        for group in chunk_group_info+            .get_chunk_groups_for_module(*entry_module)+            .await?+            .iter()+        {+            if is_isolated_merged_group_for_parent(&group.chunk_group, &parent_indices, merge_tag) {+                found = true;+                groups+                    .entry(group.index)+                    .or_insert_with(|| group.chunk_group.clone());+            }+        }+        if !found {+            bail!(+                "could not find a graph-derived isolated merged group for {}",+                entry_module.ident().to_string().await?,+            );+        }+    }++    Ok(groups.into_values().collect())+}++fn client_references_by_server_component(+    app_client_references: &ClientReferenceGraphResult,+) -> FxIndexMap<ResolvedVc<NextServerComponentModule>, Vec<ClientReference>> {+    let mut client_references_by_server_component: FxIndexMap<_, Vec<_>> = FxIndexMap::default();+    let mut framework_references = Vec::new();+    for &server_component in &app_client_references.server_component_entries {+        client_references_by_server_component+            .entry(server_component)+            .or_default();+    }+    for client_reference in &app_client_references.client_references {+        if let Some(server_component) = client_reference.server_component {+            client_references_by_server_component+                .entry(server_component)+                .or_default()+                .push(*client_reference);+        } else {+            framework_references.push(*client_reference);+        }+    }+    // Framework components need to go into first layout segment.+    if let Some((_, list)) = client_references_by_server_component.first_mut() {+        list.extend(framework_references);+    }+    client_references_by_server_component+}+ #[turbo_tasks::value] pub struct ClientReferencesChunks {     #[bincode(with = "turbo_bincode::indexmap")]@@ -142,28 +281,8 @@ pub async fn get_app_client_references_chunks(             // }             // .cell())         } else {-            let mut client_references_by_server_component: FxIndexMap<_, Vec<_>> =-                FxIndexMap::default();-            let mut framework_reference_types = Vec::new();-            for &server_component in app_client_references.server_component_entries.iter() {-                client_references_by_server_component-                    .entry(server_component)-                    .or_default();-            }-            for client_reference in app_client_references.client_references.iter() {-                if let Some(server_component) = client_reference.server_component {-                    client_references_by_server_component-                        .entry(server_component)-                        .or_default()-                        .push(client_reference.ty);-                } else {-                    framework_reference_types.push(client_reference.ty);-                }-            }-            // Framework components need to go into first layout segment-            if let Some((_, list)) = client_references_by_server_component.first_mut() {-                list.extend(framework_reference_types);-            }+            let client_references_by_server_component =+                client_references_by_server_component(&app_client_references);              let chunk_group_info = module_graph.chunk_group_info(); @@ -181,32 +300,35 @@ pub async fn get_app_client_references_chunks(             let mut client_component_ssr_chunks = FxIndexMap::default();             let mut client_component_client_chunks = FxIndexMap::default(); -            for (server_component, client_reference_types) in+            for (server_component, client_references) in                 client_references_by_server_component.into_iter()             {-                let parent_chunk_group = *chunk_group_info-                    .get_index_of(ChunkGroupKey::Shared(ResolvedVc::upcast(server_component)))-                    .await?;-                 let base_ident = server_component.ident().owned().await?;                  let server_path = server_component.server_path().owned().await?;                 let is_layout = server_path.file_stem() == Some("layout");                 let server_component_path = server_path.to_string_ref().await?; -                let ssr_modules = client_reference_types+                let ssr_modules = client_references                     .iter()-                    .map(async |client_reference_ty| {-                        Ok(match client_reference_ty {+                    .map(async |client_reference| {+                        let parent_module = client_reference.parent_module;+                        Ok(match client_reference.ty {                             ClientReferenceType::EcmascriptClientReference(                                 ecmascript_client_reference,                             ) => {                                 let ecmascript_client_reference_ref =                                     ecmascript_client_reference.await?; -                                Some(ResolvedVc::upcast(-                                    ecmascript_client_reference_ref.ssr_module,-                                ))+                                Some(ClientReferenceWithContext {+                                    entry_module: ResolvedVc::upcast(+                                        ecmascript_client_reference_ref.ssr_module,+                                    ),+                                    source_module: parent_module,+                                    server_component: client_reference+                                        .server_component+                                        .map(ResolvedVc::upcast),+                                })                             }                             _ => None,                         })@@ -217,73 +339,105 @@ pub async fn get_app_client_references_chunks(                 let ssr_chunk_group = if !ssr_modules.is_empty()                     && let Some(ssr_chunking_context) = ssr_chunking_context                 {-                    let availability_info = current_ssr_chunk_group.await?.availability_info;-                    let _span = tracing::info_span!(+                    async {+                        let groups = derived_isolated_merged_groups(+                            chunk_group_info,+                            &ssr_modules,+                            &SSR_MERGE_TAG,+                        )+                        .await?;+                        // All groups of the segment are chunked together, by a single task.+                        anyhow::Ok(if groups.is_empty() {+                            None+                        } else {+                            Some(+                                ssr_chunking_context+                                    .chunk_group(+                                        base_ident+                                            .clone()+                                            .with_modifier(rcstr!("ssr modules"))+                                            .into_vc(),+                                        groups,+                                        module_graph,+                                        current_ssr_chunk_group.await?.availability_info,+                                    )+                                    .to_resolved()+                                    .await?,+                            )+                        })+                    }+                    .instrument(tracing::info_span!(                         "server side rendering",                         layout_segment = display(&server_component_path),-                    )-                    .entered();--                    Some(-                        ssr_chunking_context.chunk_group(-                            base_ident-                                .clone()-                                .with_modifier(rcstr!("ssr modules"))-                                .into_vc(),-                            smallvec![ChunkGroup::IsolatedMerged {-                                parent: parent_chunk_group,-                                merge_tag: ecmascript_client_reference_merge_tag_ssr(),-                                entries: ssr_modules,-                            }],-                            module_graph,-                            availability_info,-                        ),-                    )+                    ))+                    .await?                 } else {                     None                 }; -                let client_modules = client_reference_types+                let client_modules = client_references                     .iter()-                    .map(async |client_reference_ty| {-                        Ok(match client_reference_ty {-                            ClientReferenceType::EcmascriptClientReference(-                                ecmascript_client_reference,-                            ) => {-                                ResolvedVc::upcast(ecmascript_client_reference.await?.client_module)-                            }-                            ClientReferenceType::CssClientReference(css_client_reference) => {-                                ResolvedVc::upcast(*css_client_reference)-                            }+                    .map(async |client_reference| {+                        let parent_module = client_reference.parent_module;+                        Ok(ClientReferenceWithContext {+                            entry_module: match client_reference.ty {+                                ClientReferenceType::EcmascriptClientReference(+                                    ecmascript_client_reference,+                                ) => ResolvedVc::upcast(+                                    ecmascript_client_reference.await?.client_module,+                                ),+                                ClientReferenceType::CssClientReference(css_client_reference) => {+                                    ResolvedVc::upcast(css_client_reference)+                                }+                            },+                            source_module: parent_module,+                            server_component: client_reference+                                .server_component+                                .map(ResolvedVc::upcast),                         })                     })                     .try_join()                     .await?;                 let client_chunk_group = if !client_modules.is_empty() {-                    let availability_info = current_client_chunk_group.await?.availability_info;-                    let _span = tracing::info_span!(+                    async {+                        let groups = derived_isolated_merged_groups(+                            chunk_group_info,+                            &client_modules,+                            &CLIENT_MERGE_TAG,+                        )+                        .await?;+                        // All groups of the segment are chunked together, by a single task.+                        anyhow::Ok(if groups.is_empty() {+                            None+                        } else {+                            Some(+                                client_chunking_context+                                    .chunk_group(+                                        base_ident+                                            .clone()+                                            .with_modifier(rcstr!("client modules"))+                                            .into_vc(),+                                        groups,+                                        module_graph,+                                        current_client_chunk_group.await?.availability_info,+                                    )+                                    .to_resolved()+                                    .await?,+                            )+                        })+                    }+                    .instrument(tracing::info_span!(                         "client side rendering",                         layout_segment = display(&server_component_path),-                    )-                    .entered();--                    Some(client_chunking_context.chunk_group(-                        base_ident.with_modifier(rcstr!("client modules")).into_vc(),-                        smallvec![ChunkGroup::IsolatedMerged {-                            parent: parent_chunk_group,-                            merge_tag: ecmascript_client_reference_merge_tag(),-                            entries: client_modules,-                        }],-                        module_graph,-                        availability_info,                     ))+                    .await?                 } else {                     None                 };                  if let Some(client_chunk_group) = client_chunk_group {                     let client_chunk_group = current_client_chunk_group-                        .concatenate(client_chunk_group)+                        .concatenate(*client_chunk_group)                         .to_resolved()                         .await?; @@ -297,19 +451,19 @@ pub async fn get_app_client_references_chunks(                         .await?;                     layout_segment_client_chunks.insert(server_component, assets); -                    for &client_reference_ty in client_reference_types.iter() {+                    for client_reference in &client_references {                         if let ClientReferenceType::EcmascriptClientReference(_) =-                            client_reference_ty+                            client_reference.ty                         {                             client_component_client_chunks-                                .insert(client_reference_ty, client_chunk_group);+                                .insert(client_reference.ty, client_chunk_group);                         }                     }                 }                  if let Some(ssr_chunk_group) = ssr_chunk_group {                     let ssr_chunk_group = current_ssr_chunk_group-                        .concatenate(ssr_chunk_group)+                        .concatenate(*ssr_chunk_group)                         .to_resolved()                         .await?; @@ -321,11 +475,11 @@ pub async fn get_app_client_references_chunks(                         .output_assets_with_referenced()                         .to_resolved()                         .await?;-                    for &client_reference_ty in client_reference_types.iter() {+                    for client_reference in &client_references {                         if let ClientReferenceType::EcmascriptClientReference(_) =-                            client_reference_ty+                            client_reference.ty                         {-                            client_component_ssr_chunks.insert(client_reference_ty, assets);+                            client_component_ssr_chunks.insert(client_reference.ty, assets);                         }                     }                 }@@ -345,8 +499,8 @@ pub async fn get_app_client_references_chunks(  /// Flattens all client-side output assets from `client_references_chunks` so the /// page's HMR chunk list can subscribe to updates for chunks built outside the-/// entry's own module graph (each `chunk_group(IsolatedMerged)` call for a-/// client component group generates chunks separately).+/// entry's own module graph (each layout segment's isolated merged client+/// reference groups are chunked separately from the entry). #[turbo_tasks::function] pub async fn get_client_references_chunks_for_hmr(     client_references_chunks: Vc<ClientReferencesChunks>,@@ -376,3 +530,56 @@ pub async fn get_client_references_chunks_for_hmr(     // (Node.js/Edge), not in the browser, so they don't belong in the client HMR chunk list.     Ok(Vc::cell(extras.into_iter().collect())) }++#[cfg(test)]+mod tests {+    use turbo_tasks::FxIndexSet;+    use turbopack_core::module_graph::chunk_group_info::ChunkGroup;++    use crate::{+        next_app::app_client_references_chunks::{+            is_isolated_merged_group_for_parent, relevant_parent_indices,+        },+        next_client_reference::{CLIENT_MERGE_TAG, SSR_MERGE_TAG},+    };++    #[test]+    fn parent_indices_prefer_a_non_empty_output_intersection() {+        let source = FxIndexSet::from_iter([1, 2]);++        assert_eq!(+            relevant_parent_indices(source.clone(), Some(&FxIndexSet::from_iter([2, 3]))),+            FxIndexSet::from_iter([2]),+        );+        assert_eq!(+            relevant_parent_indices(source.clone(), Some(&FxIndexSet::from_iter([3]))),+            source,+        );+        assert_eq!(relevant_parent_indices(source.clone(), None), source,);+    }++    #[test]+    fn isolated_merged_groups_must_match_parent_and_tag() {+        let group = ChunkGroup::IsolatedMerged {+            parent: 7,+            merge_tag: CLIENT_MERGE_TAG,+            entries: Vec::new(),+        };++        assert!(is_isolated_merged_group_for_parent(+            &group,+            &FxIndexSet::from_iter([7]),+            &CLIENT_MERGE_TAG,+        ));+        assert!(!is_isolated_merged_group_for_parent(+            &group,+            &FxIndexSet::from_iter([8]),+            &CLIENT_MERGE_TAG,+        ));+        assert!(!is_isolated_merged_group_for_parent(+            &group,+            &FxIndexSet::from_iter([7]),+            &SSR_MERGE_TAG,+        ));+    }+}
crates/next-core/src/next_client_reference/css_client_reference/css_client_reference_module.rs3 + / 1 −
@@ -13,6 +13,8 @@ use turbopack_core::{ }; use turbopack_css::chunk::CssChunkPlaceable; +use crate::next_client_reference::CLIENT_MERGE_TAG;+ /// A [`CssClientReferenceModule`] is a marker module used to indicate which /// client reference should appear in the client reference manifest. #[turbo_tasks::value]@@ -106,7 +108,7 @@ impl ModuleReference for CssClientReference {     fn chunking_type(&self) -> Option<ChunkingType> {         Some(ChunkingType::Isolated {             _ty: ChunkGroupType::Evaluated,-            merge_tag: Some(rcstr!("client")),+            merge_tag: Some(CLIENT_MERGE_TAG),         })     } }
crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_module.rs4 + / 9 −
@@ -34,6 +34,8 @@ use turbopack_ecmascript::{     utils::StringifyJs, }; +use crate::next_client_reference::{CLIENT_MERGE_TAG, SSR_MERGE_TAG};+ /// A [`EcmascriptClientReferenceModule`] is used in RSC to represent /// a client or SSR asset. #[turbo_tasks::value]@@ -186,13 +188,6 @@ impl EcmascriptClientReferenceModule {     } } -pub fn ecmascript_client_reference_merge_tag() -> RcStr {-    rcstr!("client")-}-pub fn ecmascript_client_reference_merge_tag_ssr() -> RcStr {-    rcstr!("ssr")-}- #[turbo_tasks::value_impl] impl Module for EcmascriptClientReferenceModule {     #[turbo_tasks::function]@@ -229,7 +224,7 @@ impl Module for EcmascriptClientReferenceModule {                 EcmascriptClientReference::new(                     *ResolvedVc::upcast(*client_module),                     ChunkGroupType::Evaluated,-                    Some(ecmascript_client_reference_merge_tag()),+                    Some(CLIENT_MERGE_TAG),                     rcstr!("ecmascript client reference to client"),                 )                 .to_resolved()@@ -239,7 +234,7 @@ impl Module for EcmascriptClientReferenceModule {                 EcmascriptClientReference::new(                     *ResolvedVc::upcast(*ssr_module),                     ChunkGroupType::Entry,-                    Some(ecmascript_client_reference_merge_tag_ssr()),+                    Some(SSR_MERGE_TAG),                     rcstr!("ecmascript client reference to ssr"),                 )                 .to_resolved()
crates/next-core/src/next_client_reference/mod.rs8 + / 0 −
@@ -1,7 +1,15 @@+use turbo_rcstr::{RcStr, rcstr};+ pub(crate) mod css_client_reference; pub(crate) mod ecmascript_client_reference; pub(crate) mod visit_client_reference; +/// Merge tag of the isolated chunk groups for the client side of client references (CSS and+/// ecmascript).+pub const CLIENT_MERGE_TAG: RcStr = rcstr!("client");+/// Merge tag of the isolated chunk groups for the SSR side of ecmascript client references.+pub const SSR_MERGE_TAG: RcStr = rcstr!("ssr");+ pub use css_client_reference::{     css_client_reference_module::CssClientReferenceModule,     css_client_reference_transition::NextCssClientReferenceTransition,
crates/next-core/src/next_client_reference/visit_client_reference.rs3 + / 0 −
@@ -27,6 +27,9 @@ use crate::{     Copy, Clone, Eq, PartialEq, Hash, Debug, ValueDebugFormat, NonLocalValue, Encode, Decode, )] pub struct ClientReference {+    /// The module whose reference creates the client/SSR chunk group.+    pub parent_module: ResolvedVc<Box<dyn Module>>,+    /// The server component used to order this reference in the output.     pub server_component: Option<ResolvedVc<NextServerComponentModule>>,     pub ty: ClientReferenceType, }
crates/next-core/src/next_server_utility/server_utility_reference.rs2 + / 5 −
@@ -1,5 +1,3 @@-use std::sync::LazyLock;- use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ResolvedVc, ValueToString, Vc}; use turbopack_core::{@@ -24,8 +22,7 @@ impl NextServerUtilityModuleReference {     } } -pub static NEXT_SERVER_UTILITY_MERGE_TAG: LazyLock<RcStr> =-    LazyLock::new(|| rcstr!("next-server-utility"));+pub const NEXT_SERVER_UTILITY_MERGE_TAG: RcStr = rcstr!("next-server-utility");  #[turbo_tasks::value_impl] impl ModuleReference for NextServerUtilityModuleReference {@@ -37,7 +34,7 @@ impl ModuleReference for NextServerUtilityModuleReference {     fn chunking_type(&self) -> Option<ChunkingType> {         Some(ChunkingType::Shared {             inherit_async: true,-            merge_tag: Some(NEXT_SERVER_UTILITY_MERGE_TAG.clone()),+            merge_tag: Some(NEXT_SERVER_UTILITY_MERGE_TAG),         })     } 
test/e2e/app-dir/layout-segment-chunk-sharing/layout-segment-chunk-sharing.test.ts57 + / 2 −
@@ -141,6 +141,7 @@ describe('layout-segment-chunk-sharing', () => {         )          mcpPort = await findPort()+        let traceServerOutput = ''         traceServerProcess = spawn(           'node',           [@@ -152,7 +153,37 @@ describe('layout-segment-chunk-sharing', () => {             '--mcp-port',             String(mcpPort),           ],-          { stdio: 'inherit' }+          { stdio: 'pipe' }+        )+        const recordTraceServerOutput = (chunk: Buffer) => {+          const text = chunk.toString()+          traceServerOutput += text+          process.stdout.write(text)+        }+        traceServerProcess.stdout?.on('data', recordTraceServerOutput)+        traceServerProcess.stderr?.on('data', recordTraceServerOutput)++        // The MCP server can answer while the trace is still being read, so+        // wait for the reader's end-of-file summary before querying spans.+        // Large traces report `Initial read completed`; smaller ones report+        // only their span count.+        await retry(+          async () => {+            const output = traceServerOutput.replace(+              // eslint-disable-next-line no-control-regex+              /\x1b\[[0-9;]*[A-Za-z]/g,+              ''+            )+            if (+              !/Initial read completed|(?:^|[\r\n])\d+ spans(?:[\r\n]|$)/.test(+                output+              )+            ) {+              throw new Error('Trace file is still being read')+            }+          },+          120_000,+          500         )          // Wait for the MCP HTTP server to be ready.@@ -177,7 +208,7 @@ describe('layout-segment-chunk-sharing', () => {           30_000,           500         )-      }, 120_000)+      }, 180_000)        afterAll(async () => {         if (traceServerProcess?.pid) {@@ -231,6 +262,30 @@ describe('layout-segment-chunk-sharing', () => {           1         )       })++      // The root layout's client references (e.g. the framework's layout+      // router) belong to one isolated merged chunk group per page that renders+      // the layout. Those groups are chunked together, so the root layout's+      // client modules and its SSR modules are each chunked by a single task,+      // instead of once per group (`client modules 0`, `client modules 1`, ...).+      it('chunks all client reference groups of a segment in one task', async () => {+        const spans = await findSpans(+          'app/layout.tsx [app-rsc] (ecmascript, Next.js Server Component, '+        )+        const chunkingOf = (context: string, modifier: string) =>+          spans.filter((name) =>+            new RegExp(+              `^turbopack_${context}::chunking_context chunking \\[project\\]/(?:.*/)?app/layout\\.tsx \\[app-rsc\\] \\(ecmascript, Next\\.js Server Component, ${modifier}(?: \\d+)?\\)$`+            ).test(name)+          )++        expect(chunkingOf('browser', 'client modules')).toEqual([+          expect.stringMatching(/, client modules\)$/),+        ])+        expect(chunkingOf('nodejs', 'ssr modules')).toEqual([+          expect.stringMatching(/, ssr modules\)$/),+        ])+      })     }   ) })
turbopack/crates/turbopack-core/src/chunk/chunk_group.rs11 + / 4 −
@@ -313,16 +313,23 @@ async fn chunk_group_content_operation(         collecting_modules: FxIndexSet::default(),     }; +    let chunk_group_info = module_graph.chunk_group_info();+    let chunk_group_indices = chunk_groups+        .iter()+        .map(async |chunk_group| Ok(*chunk_group_info.get_index_of(chunk_group.key()).await?))+        .try_join()+        .await?;+    let chunk_group_info = chunk_group_info.await?;     let available_modules = match availability_info.available_modules() {         Some(available_modules) => Some(available_modules.snapshot().await?),         None => None,     }; -    // The entries of all groups, in list order. An entry shared by several groups is traversed-    // once.-    let entries = chunk_groups+    // The ordered entries of all groups, in list order. An entry shared by several groups is+    // traversed once.+    let entries = chunk_group_indices         .iter()-        .flat_map(|chunk_group| chunk_group.entries())+        .flat_map(|&index| module_batches_graph.get_ordered_entries(&chunk_group_info, index))         .collect::<FxIndexSet<_>>()         .into_iter()         .map(|entry| module_batches_graph.get_entry_index(entry))
turbopack/crates/turbopack-core/src/module_graph/chunk_group_info.rs59 + / 0 −
@@ -82,6 +82,18 @@ impl Hash for RoaringBitmapWrapper { #[turbo_tasks::value(transparent, cell = "keyed")] pub struct ModuleToChunkGroups(FxHashMap<ResolvedVc<Box<dyn Module>>, RoaringBitmapWrapper>); +#[turbo_tasks::value]+pub struct ChunkGroupWithIndex {+    pub index: u32,+    pub chunk_group: ChunkGroup,+}++#[turbo_tasks::value(transparent)]+pub struct ChunkGroups(Vec<ChunkGroupWithIndex>);++#[turbo_tasks::value(transparent)]+pub struct OptionChunkGroupWithIndex(Option<ChunkGroupWithIndex>);+ #[turbo_tasks::value] pub struct ChunkGroupInfo {     pub module_chunk_groups: ResolvedVc<ModuleToChunkGroups>,@@ -142,6 +154,53 @@ impl ChunkGroupInfo {             }         }     }++    /// Returns the exact chunk groups that contain `module`, as derived while traversing the module+    /// graph.+    #[turbo_tasks::function]+    pub async fn get_chunk_groups_for_module(+        &self,+        module: ResolvedVc<Box<dyn Module>>,+    ) -> Result<Vc<ChunkGroups>> {+        let module_chunk_groups = self.module_chunk_groups.await?;+        let Some(groups) = module_chunk_groups.get(&module) else {+            return Ok(Vc::cell(Vec::new()));+        };+        Ok(Vc::cell(+            groups+                .iter()+                .map(|index| ChunkGroupWithIndex {+                    index,+                    chunk_group: self.chunk_groups[index as usize].clone(),+                })+                .collect(),+        ))+    }++    /// Returns the shared merged chunk group with `merge_tag` that `parent` is the parent of, if+    /// the graph produced one.+    ///+    /// A merge tag collects everything reachable through the tagged shared references of one+    /// parent group into a single group, so there is at most one such group per parent and tag.+    #[turbo_tasks::function]+    pub fn get_shared_merged_chunk_group(+        &self,+        parent: usize,+        merge_tag: RcStr,+    ) -> Vc<OptionChunkGroupWithIndex> {+        let key = ChunkGroupKey::SharedMerged {+            parent: ChunkGroupId::from(parent),+            merge_tag,+        };+        Vc::cell(+            self.chunk_group_keys+                .get_index_of(&key)+                .map(|index| ChunkGroupWithIndex {+                    index: index as u32,+                    chunk_group: self.chunk_groups[index].clone(),+                }),+        )+    } }  /// Per-entry chunking heuristics.