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
42fn 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#[derive(Clone, Copy)]
73struct ClientReferenceWithContext {
74 entry_module: ResolvedVc<Box<dyn Module>>,
76 source_module: ResolvedVc<Box<dyn Module>>,
78 server_component: Option<ResolvedVc<Box<dyn Module>>>,
81}
82
83async 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 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#[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 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 } 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 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 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#[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 extras.extend(chunk_group.all_assets().await?.iter().copied());
528 }
529 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}