Skip to main content

next_core/next_app/
app_client_references_chunks.rs

1use anyhow::{Result, bail};
2use smallvec::SmallVec;
3use tracing::Instrument;
4use turbo_rcstr::{RcStr, rcstr};
5use turbo_tasks::{
6    FxIndexMap, FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToString,
7    ValueToStringRef, Vc,
8};
9use turbopack_core::{
10    chunk::{ChunkGroupResult, ChunkingContext, availability_info::AvailabilityInfo},
11    module::Module,
12    module_graph::{
13        ModuleGraph,
14        chunk_group_info::{ChunkGroup, ChunkGroupInfo, ChunkGroupKey},
15    },
16    output::{OutputAsset, OutputAssets, OutputAssetsWithReferenced},
17};
18
19use crate::{
20    next_client_reference::{
21        CLIENT_MERGE_TAG, ClientReference, ClientReferenceType, SSR_MERGE_TAG,
22        visit_client_reference::ClientReferenceGraphResult,
23    },
24    next_server_component::server_component_module::NextServerComponentModule,
25};
26
27fn is_isolated_merged_group_for_parent(
28    chunk_group: &ChunkGroup,
29    parent_indices: &FxIndexSet<u32>,
30    merge_tag: &RcStr,
31) -> bool {
32    matches!(
33        chunk_group,
34        ChunkGroup::IsolatedMerged {
35            parent,
36            merge_tag: group_merge_tag,
37            ..
38        } if group_merge_tag == merge_tag && parent_indices.contains(&(*parent as u32))
39    )
40}
41
42/// The chunk groups a client reference's merged group may hang off.
43///
44/// A client reference is reached from its source module, which can sit in several chunk groups
45/// when the reference is shared between routes. When the reference belongs to a server component,
46/// the relevant groups are the ones that contain that server component too.
47///
48/// An empty intersection is not an error: in a development module graph the source module and the
49/// server component do not necessarily share a chunk group, and framework-level references have no
50/// server component at all. Falling back to the source module's own groups is the conservative
51/// choice -- the reference is still chunked, just not narrowed to the component's groups.
52fn relevant_parent_indices(
53    source_indices: FxIndexSet<u32>,
54    server_component_indices: Option<&FxIndexSet<u32>>,
55) -> FxIndexSet<u32> {
56    let Some(server_component_indices) = server_component_indices else {
57        return source_indices;
58    };
59    let intersection = source_indices
60        .intersection(server_component_indices)
61        .copied()
62        .collect::<FxIndexSet<_>>();
63    if intersection.is_empty() {
64        source_indices
65    } else {
66        intersection
67    }
68}
69
70/// A client reference, the module it is reached from, and the server component that owns it (if
71/// any).
72#[derive(Clone, Copy)]
73struct ClientReferenceWithContext {
74    /// The module that is chunked for this client reference.
75    entry_module: ResolvedVc<Box<dyn Module>>,
76    /// The module the client reference is reached from.
77    source_module: ResolvedVc<Box<dyn Module>>,
78    /// The server component this client reference belongs to, if it belongs to one. Framework
79    /// references do not.
80    server_component: Option<ResolvedVc<Box<dyn Module>>>,
81}
82
83/// Returns the keys of the graph-derived isolated merged groups (tagged `merge_tag`) that chunk
84/// `references`. Chunking looks the groups up by key, so their entries come from the graph.
85async fn derived_isolated_merged_groups(
86    chunk_group_info: Vc<ChunkGroupInfo>,
87    references: &[ClientReferenceWithContext],
88    merge_tag: &RcStr,
89) -> Result<SmallVec<[ChunkGroupKey; 1]>> {
90    let mut groups = FxIndexMap::default();
91
92    for &ClientReferenceWithContext {
93        entry_module,
94        source_module,
95        server_component,
96    } in references
97    {
98        let source_groups = chunk_group_info
99            .get_chunk_groups_for_module(*source_module)
100            .await?;
101        let server_component_indices = if let Some(server_component) = server_component {
102            Some(
103                chunk_group_info
104                    .get_chunk_groups_for_module(*server_component)
105                    .await?
106                    .iter()
107                    .map(|group| group.index)
108                    .collect::<FxIndexSet<_>>(),
109            )
110        } else {
111            None
112        };
113        let source_indices = source_groups.iter().map(|group| group.index).collect();
114        let parent_indices =
115            relevant_parent_indices(source_indices, server_component_indices.as_ref());
116
117        let mut found = false;
118        for group in chunk_group_info
119            .get_chunk_groups_for_module(*entry_module)
120            .await?
121            .iter()
122        {
123            if is_isolated_merged_group_for_parent(&group.chunk_group, &parent_indices, merge_tag) {
124                found = true;
125                groups
126                    .entry(group.index)
127                    .or_insert_with(|| group.chunk_group.key());
128            }
129        }
130        if !found {
131            bail!(
132                "could not find a graph-derived isolated merged group for {}",
133                entry_module.ident().to_string().await?,
134            );
135        }
136    }
137
138    Ok(groups.into_values().collect())
139}
140
141fn client_references_by_server_component(
142    app_client_references: &ClientReferenceGraphResult,
143) -> FxIndexMap<ResolvedVc<NextServerComponentModule>, Vec<ClientReference>> {
144    let mut client_references_by_server_component: FxIndexMap<_, Vec<_>> = FxIndexMap::default();
145    let mut framework_references = Vec::new();
146    for &server_component in &app_client_references.server_component_entries {
147        client_references_by_server_component
148            .entry(server_component)
149            .or_default();
150    }
151    for client_reference in &app_client_references.client_references {
152        if let Some(server_component) = client_reference.server_component {
153            client_references_by_server_component
154                .entry(server_component)
155                .or_default()
156                .push(*client_reference);
157        } else {
158            framework_references.push(*client_reference);
159        }
160    }
161    // Framework components need to go into first layout segment.
162    if let Some((_, list)) = client_references_by_server_component.first_mut() {
163        list.extend(framework_references);
164    }
165    client_references_by_server_component
166}
167
168#[turbo_tasks::value]
169pub struct ClientReferencesChunks {
170    #[bincode(with = "turbo_bincode::indexmap")]
171    pub client_component_client_chunks:
172        FxIndexMap<ClientReferenceType, ResolvedVc<ChunkGroupResult>>,
173    #[bincode(with = "turbo_bincode::indexmap")]
174    pub client_component_ssr_chunks:
175        FxIndexMap<ClientReferenceType, ResolvedVc<OutputAssetsWithReferenced>>,
176    #[bincode(with = "turbo_bincode::indexmap")]
177    pub layout_segment_client_chunks:
178        FxIndexMap<ResolvedVc<NextServerComponentModule>, ResolvedVc<OutputAssetsWithReferenced>>,
179}
180
181/// Computes all client references chunks.
182///
183/// This returns a map from client reference type to the chunks that the reference
184/// type needs to load.
185#[turbo_tasks::function]
186pub async fn get_app_client_references_chunks(
187    app_client_references: Vc<ClientReferenceGraphResult>,
188    module_graph: Vc<ModuleGraph>,
189    client_chunking_context: Vc<Box<dyn ChunkingContext>>,
190    client_availability_info: AvailabilityInfo,
191    ssr_chunking_context: Option<Vc<Box<dyn ChunkingContext>>>,
192) -> Result<Vc<ClientReferencesChunks>> {
193    async move {
194        // TODO Reconsider this. Maybe it need to be true in production.
195        let separate_chunk_group_per_client_reference = false;
196        let app_client_references = app_client_references.await?;
197        if separate_chunk_group_per_client_reference {
198            todo!();
199            // let app_client_references_chunks: Vec<(_, (_, Option<_>))> = app_client_references
200            //     .client_references
201            //     .iter()
202            //     .map(|client_reference| async move {
203            //         Ok((
204            //             client_reference.ty,
205            //             match client_reference.ty {
206            //                 ClientReferenceType::EcmascriptClientReference(
207            //                     ecmascript_client_reference,
208            //                 ) => {
209            //                     let ecmascript_client_reference_ref =
210            //                         ecmascript_client_reference.await?;
211
212            //                     let client_chunk_group = client_chunking_context
213            //                         .root_chunk_group(
214            //                             module_graph,
215            //                             *ResolvedVc::upcast(
216            //                                 ecmascript_client_reference_ref.client_module,
217            //                             ),
218            //                         )
219            //                         .await?;
220
221            //                     (
222            //                         (
223            //                             client_chunk_group.assets,
224            //                             client_chunk_group.availability_info,
225            //                         ),
226            //                         if let Some(ssr_chunking_context) = ssr_chunking_context {
227            //                             let ssr_chunk_group = ssr_chunking_context
228            //                                 .root_chunk_group(
229            //                                     *ResolvedVc::upcast(
230            //                                         ecmascript_client_reference_ref.ssr_module,
231            //                                     ),
232            //                                     module_graph,
233            //                                 )
234            //                                 .await?;
235
236            //                             Some((
237            //                                 ssr_chunk_group.assets,
238            //                                 ssr_chunk_group.availability_info,
239            //                             ))
240            //                         } else {
241            //                             None
242            //                         },
243            //                     )
244            //                 }
245            //                 ClientReferenceType::CssClientReference(css_client_reference) => {
246            //                     let client_chunk_group = client_chunking_context
247            //                         .root_chunk_group(
248            //                             *ResolvedVc::upcast(css_client_reference),
249            //                             module_graph,
250            //                         )
251            //                         .await?;
252
253            //                     (
254            //                         (
255            //                             client_chunk_group.assets,
256            //                             client_chunk_group.availability_info,
257            //                         ),
258            //                         None,
259            //                     )
260            //                 }
261            //             },
262            //         ))
263            //     })
264            //     .try_join()
265            //     .await?;
266
267            // Ok(ClientReferencesChunks {
268            //     client_component_client_chunks: app_client_references_chunks
269            //         .iter()
270            //         .map(|&(client_reference_ty, (client_chunks, _))| {
271            //             (client_reference_ty, client_chunks)
272            //         })
273            //         .collect(),
274            //     client_component_ssr_chunks: app_client_references_chunks
275            //         .iter()
276            //         .flat_map(|&(client_reference_ty, (_, ssr_chunks))| {
277            //             ssr_chunks.map(|ssr_chunks| (client_reference_ty, ssr_chunks))
278            //         })
279            //         .collect(),
280            //     layout_segment_client_chunks: FxIndexMap::default(),
281            // }
282            // .cell())
283        } else {
284            let client_references_by_server_component =
285                client_references_by_server_component(&app_client_references);
286
287            let chunk_group_info = module_graph.chunk_group_info();
288
289            let mut current_client_chunk_group = ChunkGroupResult {
290                assets: ResolvedVc::cell(vec![]),
291                referenced_assets: ResolvedVc::cell(vec![]),
292                references: ResolvedVc::cell(vec![]),
293                availability_info: client_availability_info,
294                chunk_group_bootstrap_params: None,
295            }
296            .resolved_cell();
297            let mut current_ssr_chunk_group = ChunkGroupResult::empty_resolved();
298
299            let mut layout_segment_client_chunks = FxIndexMap::default();
300            let mut client_component_ssr_chunks = FxIndexMap::default();
301            let mut client_component_client_chunks = FxIndexMap::default();
302
303            for (server_component, client_references) in
304                client_references_by_server_component.into_iter()
305            {
306                let base_ident = server_component.ident().owned().await?;
307
308                let server_path = server_component.server_path().owned().await?;
309                let is_layout = server_path.file_stem() == Some("layout");
310                let server_component_path = server_path.to_string_ref().await?;
311
312                let ssr_modules = client_references
313                    .iter()
314                    .map(async |client_reference| {
315                        let parent_module = client_reference.parent_module;
316                        Ok(match client_reference.ty {
317                            ClientReferenceType::EcmascriptClientReference(
318                                ecmascript_client_reference,
319                            ) => {
320                                let ecmascript_client_reference_ref =
321                                    ecmascript_client_reference.await?;
322
323                                Some(ClientReferenceWithContext {
324                                    entry_module: ResolvedVc::upcast(
325                                        ecmascript_client_reference_ref.ssr_module,
326                                    ),
327                                    source_module: parent_module,
328                                    server_component: client_reference
329                                        .server_component
330                                        .map(ResolvedVc::upcast),
331                                })
332                            }
333                            _ => None,
334                        })
335                    })
336                    .try_flat_join()
337                    .await?;
338
339                let ssr_chunk_group = if !ssr_modules.is_empty()
340                    && let Some(ssr_chunking_context) = ssr_chunking_context
341                {
342                    async {
343                        let groups = derived_isolated_merged_groups(
344                            chunk_group_info,
345                            &ssr_modules,
346                            &SSR_MERGE_TAG,
347                        )
348                        .await?;
349                        // All groups of the segment are chunked together, by a single task.
350                        anyhow::Ok(if groups.is_empty() {
351                            None
352                        } else {
353                            Some(
354                                ssr_chunking_context
355                                    .chunk_group(
356                                        base_ident
357                                            .clone()
358                                            .with_modifier(rcstr!("ssr modules"))
359                                            .into_vc(),
360                                        groups,
361                                        module_graph,
362                                        current_ssr_chunk_group.await?.availability_info,
363                                    )
364                                    .to_resolved()
365                                    .await?,
366                            )
367                        })
368                    }
369                    .instrument(tracing::info_span!(
370                        "server side rendering",
371                        layout_segment = display(&server_component_path),
372                    ))
373                    .await?
374                } else {
375                    None
376                };
377
378                let client_modules = client_references
379                    .iter()
380                    .map(async |client_reference| {
381                        let parent_module = client_reference.parent_module;
382                        Ok(ClientReferenceWithContext {
383                            entry_module: match client_reference.ty {
384                                ClientReferenceType::EcmascriptClientReference(
385                                    ecmascript_client_reference,
386                                ) => ResolvedVc::upcast(
387                                    ecmascript_client_reference.await?.client_module,
388                                ),
389                                ClientReferenceType::CssClientReference(css_client_reference) => {
390                                    ResolvedVc::upcast(css_client_reference)
391                                }
392                            },
393                            source_module: parent_module,
394                            server_component: client_reference
395                                .server_component
396                                .map(ResolvedVc::upcast),
397                        })
398                    })
399                    .try_join()
400                    .await?;
401                let client_chunk_group = if !client_modules.is_empty() {
402                    async {
403                        let groups = derived_isolated_merged_groups(
404                            chunk_group_info,
405                            &client_modules,
406                            &CLIENT_MERGE_TAG,
407                        )
408                        .await?;
409                        // All groups of the segment are chunked together, by a single task.
410                        anyhow::Ok(if groups.is_empty() {
411                            None
412                        } else {
413                            Some(
414                                client_chunking_context
415                                    .chunk_group(
416                                        base_ident
417                                            .clone()
418                                            .with_modifier(rcstr!("client modules"))
419                                            .into_vc(),
420                                        groups,
421                                        module_graph,
422                                        current_client_chunk_group.await?.availability_info,
423                                    )
424                                    .to_resolved()
425                                    .await?,
426                            )
427                        })
428                    }
429                    .instrument(tracing::info_span!(
430                        "client side rendering",
431                        layout_segment = display(&server_component_path),
432                    ))
433                    .await?
434                } else {
435                    None
436                };
437
438                if let Some(client_chunk_group) = client_chunk_group {
439                    let client_chunk_group = current_client_chunk_group
440                        .concatenate(*client_chunk_group)
441                        .to_resolved()
442                        .await?;
443
444                    if is_layout {
445                        current_client_chunk_group = client_chunk_group;
446                    }
447
448                    let assets = client_chunk_group
449                        .output_assets_with_referenced()
450                        .to_resolved()
451                        .await?;
452                    layout_segment_client_chunks.insert(server_component, assets);
453
454                    for client_reference in &client_references {
455                        if let ClientReferenceType::EcmascriptClientReference(_) =
456                            client_reference.ty
457                        {
458                            client_component_client_chunks
459                                .insert(client_reference.ty, client_chunk_group);
460                        }
461                    }
462                }
463
464                if let Some(ssr_chunk_group) = ssr_chunk_group {
465                    let ssr_chunk_group = current_ssr_chunk_group
466                        .concatenate(*ssr_chunk_group)
467                        .to_resolved()
468                        .await?;
469
470                    if is_layout {
471                        current_ssr_chunk_group = ssr_chunk_group;
472                    }
473
474                    let assets = ssr_chunk_group
475                        .output_assets_with_referenced()
476                        .to_resolved()
477                        .await?;
478                    for client_reference in &client_references {
479                        if let ClientReferenceType::EcmascriptClientReference(_) =
480                            client_reference.ty
481                        {
482                            client_component_ssr_chunks.insert(client_reference.ty, assets);
483                        }
484                    }
485                }
486            }
487
488            Ok(ClientReferencesChunks {
489                client_component_client_chunks,
490                client_component_ssr_chunks,
491                layout_segment_client_chunks,
492            }
493            .cell())
494        }
495    }
496    .instrument(tracing::info_span!("process client references"))
497    .await
498}
499
500/// Flattens all client-side output assets from `client_references_chunks` so the
501/// page's HMR chunk list can subscribe to updates for chunks built outside the
502/// entry's own module graph (each layout segment's isolated merged client
503/// reference groups are chunked separately from the entry).
504#[turbo_tasks::function]
505pub async fn get_client_references_chunks_for_hmr(
506    client_references_chunks: Vc<ClientReferencesChunks>,
507) -> Result<Vc<OutputAssets>> {
508    let client_references_chunks_ref = client_references_chunks.await?;
509    let mut extras: FxIndexSet<ResolvedVc<Box<dyn OutputAsset>>> = client_references_chunks_ref
510        .layout_segment_client_chunks
511        .values()
512        .map(async |&assets| {
513            let primary = assets.primary_assets().await?;
514            Ok(primary.iter().copied().collect::<Vec<_>>())
515        })
516        .try_flat_join()
517        .await?
518        .into_iter()
519        .collect();
520    for &chunk_group in client_references_chunks_ref
521        .client_component_client_chunks
522        .values()
523    {
524        // Use all_assets() (not primary_assets()) to also follow async loader references
525        // transitively. This ensures that dynamic imports within 'use client' pages are
526        // covered by the page's HMR subscription, not just the page module itself.
527        extras.extend(chunk_group.all_assets().await?.iter().copied());
528    }
529    // client_component_ssr_chunks are intentionally excluded: they run on the server
530    // (Node.js/Edge), not in the browser, so they don't belong in the client HMR chunk list.
531    Ok(Vc::cell(extras.into_iter().collect()))
532}
533
534#[cfg(test)]
535mod tests {
536    use turbo_tasks::FxIndexSet;
537    use turbopack_core::module_graph::chunk_group_info::ChunkGroup;
538
539    use crate::{
540        next_app::app_client_references_chunks::{
541            is_isolated_merged_group_for_parent, relevant_parent_indices,
542        },
543        next_client_reference::{CLIENT_MERGE_TAG, SSR_MERGE_TAG},
544    };
545
546    #[test]
547    fn parent_indices_prefer_a_non_empty_output_intersection() {
548        let source = FxIndexSet::from_iter([1, 2]);
549
550        assert_eq!(
551            relevant_parent_indices(source.clone(), Some(&FxIndexSet::from_iter([2, 3]))),
552            FxIndexSet::from_iter([2]),
553        );
554        assert_eq!(
555            relevant_parent_indices(source.clone(), Some(&FxIndexSet::from_iter([3]))),
556            source,
557        );
558        assert_eq!(relevant_parent_indices(source.clone(), None), source,);
559    }
560
561    #[test]
562    fn isolated_merged_groups_must_match_parent_and_tag() {
563        let group = ChunkGroup::IsolatedMerged {
564            parent: 7,
565            merge_tag: CLIENT_MERGE_TAG,
566            entries: Vec::new(),
567        };
568
569        assert!(is_isolated_merged_group_for_parent(
570            &group,
571            &FxIndexSet::from_iter([7]),
572            &CLIENT_MERGE_TAG,
573        ));
574        assert!(!is_isolated_merged_group_for_parent(
575            &group,
576            &FxIndexSet::from_iter([8]),
577            &CLIENT_MERGE_TAG,
578        ));
579        assert!(!is_isolated_merged_group_for_parent(
580            &group,
581            &FxIndexSet::from_iter([7]),
582            &SSR_MERGE_TAG,
583        ));
584    }
585}