1use anyhow::Result;
2use either::Either;
3use indoc::formatdoc;
4use itertools::Itertools;
5use rustc_hash::{FxHashMap, FxHashSet};
6use serde::Serialize;
7use tracing::Instrument;
8use turbo_rcstr::{RcStr, rcstr};
9use turbo_tasks::{
10 FxIndexMap, FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToString,
11 ValueToStringRef, Vc,
12};
13use turbo_tasks_fs::{File, FileContent, FileSystemPath};
14use turbopack_core::{
15 asset::{Asset, AssetContent},
16 chunk::{
17 ChunkingContext, CrossOrigin, ModuleChunkItemIdExt, ModuleId as TurbopackModuleId,
18 OutputChunk,
19 },
20 module_graph::async_module_info::AsyncModulesInfo,
21 output::{OutputAsset, OutputAssets, OutputAssetsReference, OutputAssetsWithReferenced},
22};
23use turbopack_ecmascript::utils::StringifyJs;
24
25use crate::{
26 mode::NextMode,
27 next_app::ClientReferencesChunks,
28 next_client_reference::{ClientReferenceGraphResult, ClientReferenceType},
29 next_config::NextConfig,
30 next_manifests::{ModuleId, encode_uri_component::encode_uri_component},
31 util::NextRuntime,
32};
33
34#[derive(Serialize, Default, Debug)]
35#[serde(rename_all = "camelCase")]
36pub struct SerializedClientReferenceManifest {
37 pub module_loading: ModuleLoading,
38 pub client_modules: ManifestNode,
41 pub ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
44 #[serde(rename = "edgeSSRModuleMapping")]
46 pub edge_ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
47 pub rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
50 #[serde(rename = "edgeRscModuleMapping")]
52 pub edge_rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
53 #[serde(rename = "entryCSSFiles")]
55 pub entry_css_files: FxIndexMap<RcStr, FxIndexSet<CssResource>>,
56 #[serde(rename = "entryJSFiles")]
58 pub entry_js_files: FxIndexMap<RcStr, FxIndexSet<RcStr>>,
59}
60
61#[derive(Serialize, Debug, Clone, Eq, Hash, PartialEq)]
62pub struct CssResource {
63 pub path: RcStr,
64 pub inlined: bool,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub content: Option<RcStr>,
67}
68
69#[derive(Serialize, Default, Debug)]
70#[serde(rename_all = "camelCase")]
71pub struct ModuleLoading {
72 pub prefix: RcStr,
73 pub cross_origin: CrossOrigin,
74}
75
76#[derive(Serialize, Default, Debug, Clone)]
77#[serde(rename_all = "camelCase")]
78pub struct ManifestNode {
79 #[serde(flatten)]
81 pub module_exports: FxIndexMap<RcStr, ManifestNodeEntry>,
82}
83
84#[derive(Serialize, Debug, Clone)]
85#[serde(rename_all = "camelCase")]
86pub struct ManifestNodeEntry {
87 pub id: ModuleId,
89 pub name: RcStr,
91 pub chunks: Vec<ClientChunk>,
93 pub r#async: bool,
95}
96
97#[derive(Serialize, Debug, Clone)]
105#[serde(untagged)]
106pub enum ClientChunk {
107 Path(RcStr),
108 Merged(RcStr, Vec<RcStr>, Vec<u64>),
109}
110
111#[turbo_tasks::value(shared)]
112pub struct ClientReferenceManifest {
113 pub node_root: FileSystemPath,
114 pub client_relative_path: FileSystemPath,
115 pub entry_name: RcStr,
116 pub client_references: ResolvedVc<ClientReferenceGraphResult>,
117 pub client_references_chunks: ResolvedVc<ClientReferencesChunks>,
118 pub client_chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
119 pub ssr_chunking_context: Option<ResolvedVc<Box<dyn ChunkingContext>>>,
120 pub async_module_info: ResolvedVc<AsyncModulesInfo>,
121 pub next_config: ResolvedVc<NextConfig>,
122 pub runtime: NextRuntime,
123 pub mode: NextMode,
124}
125
126#[turbo_tasks::value_impl]
127impl OutputAssetsReference for ClientReferenceManifest {
128 #[turbo_tasks::function]
129 async fn references(self: Vc<Self>) -> Result<Vc<OutputAssetsWithReferenced>> {
130 Ok(OutputAssetsWithReferenced::from_assets(
131 *build_manifest(self).await?.references,
132 ))
133 }
134}
135
136#[turbo_tasks::value_impl]
137impl OutputAsset for ClientReferenceManifest {
138 #[turbo_tasks::function]
139 async fn path(&self) -> Result<Vc<FileSystemPath>> {
140 let normalized_manifest_entry = self.entry_name.replace("%5F", "_");
141 Ok(self
142 .node_root
143 .join(&format!(
144 "server/app{normalized_manifest_entry}_client-reference-manifest.js",
145 ))?
146 .cell())
147 }
148}
149
150#[turbo_tasks::value_impl]
151impl Asset for ClientReferenceManifest {
152 #[turbo_tasks::function]
153 async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
154 Ok(*build_manifest(self).await?.content)
155 }
156}
157
158#[turbo_tasks::value(shared)]
159struct ClientReferenceManifestResult {
160 content: ResolvedVc<AssetContent>,
161 references: ResolvedVc<OutputAssets>,
162}
163
164#[turbo_tasks::function]
165async fn build_manifest(
166 manifest: Vc<ClientReferenceManifest>,
167) -> Result<Vc<ClientReferenceManifestResult>> {
168 let ClientReferenceManifest {
169 node_root,
170 client_relative_path,
171 entry_name,
172 client_references,
173 client_references_chunks,
174 client_chunking_context,
175 ssr_chunking_context,
176 async_module_info,
177 next_config,
178 runtime,
179 mode,
180 } = &*manifest.await?;
181 let span = tracing::info_span!(
182 "build client reference manifest",
183 entry_name = display(&entry_name)
184 );
185 async move {
186 let mut entry_manifest: SerializedClientReferenceManifest = Default::default();
187 let mut references = FxIndexSet::default();
188 let prefix_path = next_config.computed_asset_prefix().owned().await?;
189 let asset_suffix_path = next_config.asset_suffix_path().owned().await?;
190 let add_deployment_id_at_runtime = *next_config
191 .should_append_server_deployment_id_at_runtime()
192 .await?;
193 let suffix_path = if !add_deployment_id_at_runtime {
194 asset_suffix_path.unwrap_or_default()
195 } else {
196 rcstr!("")
197 };
198
199 entry_manifest.module_loading.cross_origin = *next_config.cross_origin().await?;
200 let ClientReferencesChunks {
201 client_component_client_chunks,
202 layout_segment_client_chunks,
203 client_component_ssr_chunks,
204 } = &*client_references_chunks.await?;
205 let client_relative_path = client_relative_path.clone();
206 let node_root_ref = node_root.clone();
207
208 let client_references_ecmascript = client_references
209 .await?
210 .client_references
211 .iter()
212 .map(async |r| {
213 Ok(match r.ty {
214 ClientReferenceType::EcmascriptClientReference(r) => Some((r, r.await?)),
215 ClientReferenceType::CssClientReference(_) => None,
216 })
217 })
218 .try_flat_join()
219 .await?;
220
221 let async_modules = client_references_ecmascript
222 .iter()
223 .flat_map(|(r, r_val)| {
224 [
225 ResolvedVc::upcast(*r),
226 ResolvedVc::upcast(r_val.client_module),
227 ResolvedVc::upcast(r_val.ssr_module),
228 ]
229 })
230 .map(async move |asset| {
231 Ok(if async_module_info.is_async(asset).await? {
232 Some(asset)
233 } else {
234 None
235 })
236 })
237 .try_flat_join()
238 .await?;
239
240 async fn cached_chunk_paths(
241 cache: &mut FxHashMap<ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath>,
242 chunks: impl Iterator<Item = ResolvedVc<Box<dyn OutputAsset>>>,
243 ) -> Result<impl Iterator<Item = (ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath)>>
244 {
245 let results = chunks
246 .into_iter()
247 .map(|chunk| (chunk, cache.get(&chunk).cloned()))
248 .map(async |(chunk, path)| {
249 Ok(if let Some(path) = path {
250 (chunk, Either::Left(path))
251 } else {
252 (chunk, Either::Right(chunk.path().owned().await?))
253 })
254 })
255 .try_join()
256 .await?;
257
258 for (chunk, path) in &results {
259 if let Either::Right(path) = path {
260 cache.insert(*chunk, path.clone());
261 }
262 }
263 Ok(results.into_iter().map(|(chunk, path)| match path {
264 Either::Left(path) => (chunk, path),
265 Either::Right(path) => (chunk, path),
266 }))
267 }
268 let mut client_chunk_path_cache: FxHashMap<
269 ResolvedVc<Box<dyn OutputAsset>>,
270 FileSystemPath,
271 > = FxHashMap::default();
272 let mut ssr_chunk_path_cache: FxHashMap<ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath> =
273 FxHashMap::default();
274
275 let mut client_reference_chunk_paths: FxHashSet<RcStr> = FxHashSet::default();
276
277 for (client_reference_module, client_reference_module_ref) in client_references_ecmascript {
278 let app_client_reference_ty =
279 ClientReferenceType::EcmascriptClientReference(client_reference_module);
280
281 let server_path = client_reference_module_ref.server_ident.to_string().await?;
282 let client_module = client_reference_module_ref.client_module;
283 let client_chunk_item_id = client_module
284 .chunk_item_id(**client_chunking_context)
285 .await?;
286
287 let (client_chunks_paths, client_is_async) = if let Some(client_assets) =
288 client_component_client_chunks.get(&app_client_reference_ty)
289 {
290 let client_chunks = client_assets.primary_assets().await?;
291 let client_referenced_assets = client_assets.referenced_assets().await?;
292 references.extend(client_chunks.iter());
293 references.extend(client_referenced_assets.iter());
294
295 let client_chunks_paths =
296 cached_chunk_paths(&mut client_chunk_path_cache, client_chunks.iter().copied())
297 .await?;
298
299 let js_chunks = client_chunks_paths
300 .filter_map(|(chunk, chunk_path)| {
301 client_relative_path
302 .get_path_to(&chunk_path)
303 .map(|path| (chunk, path.to_string()))
304 })
305 .filter(|(_, path)| path.ends_with(".js"))
308 .collect::<Vec<_>>();
309
310 for (_, path) in &js_chunks {
311 client_reference_chunk_paths.insert(RcStr::from(path.as_str()));
312 }
313
314 let chunk_paths = js_chunks
315 .into_iter()
316 .map(async |(chunk, path)| {
317 let url = RcStr::from(format!(
318 "{}{}{}",
319 prefix_path,
320 path.split('/').map(encode_uri_component).format("/"),
321 suffix_path
322 ));
323 let components =
326 client_chunk_components(chunk, &client_relative_path).await?;
327 Ok(if components.is_empty() {
328 ClientChunk::Path(url)
329 } else {
330 let (paths, sizes) = components.into_iter().unzip();
331 ClientChunk::Merged(url, paths, sizes)
332 })
333 })
334 .try_join()
335 .await?;
336
337 let is_async = async_modules.contains(&ResolvedVc::upcast(client_module));
338
339 (chunk_paths, is_async)
340 } else {
341 (Vec::new(), false)
342 };
343
344 if let Some(ssr_chunking_context) = *ssr_chunking_context {
345 let ssr_module = client_reference_module_ref.ssr_module;
346 let ssr_chunk_item_id = ssr_module.chunk_item_id(*ssr_chunking_context).await?;
347
348 let rsc_chunk_item_id = client_reference_module
349 .chunk_item_id(*ssr_chunking_context)
350 .await?;
351
352 let (ssr_chunks_paths, ssr_is_async) = if *runtime == NextRuntime::Edge {
353 (Vec::new(), false)
358 } else if let Some(ssr_assets) =
359 client_component_ssr_chunks.get(&app_client_reference_ty)
360 {
361 let ssr_chunks = ssr_assets.primary_assets().await?;
362 let ssr_referenced_assets = ssr_assets.referenced_assets().await?;
363 references.extend(ssr_chunks.iter());
364 references.extend(ssr_referenced_assets.iter());
365
366 let ssr_chunks_paths =
367 cached_chunk_paths(&mut ssr_chunk_path_cache, ssr_chunks.iter().copied())
368 .await?;
369 let chunk_paths = ssr_chunks_paths
370 .filter_map(|(_, chunk_path)| {
371 node_root_ref
372 .get_path_to(&chunk_path)
373 .map(ToString::to_string)
374 })
375 .map(RcStr::from)
376 .collect::<Vec<_>>();
377
378 let is_async = async_modules.contains(&ResolvedVc::upcast(ssr_module));
379
380 (chunk_paths, is_async)
381 } else {
382 (Vec::new(), false)
383 };
384
385 let rsc_is_async = if *runtime == NextRuntime::Edge {
386 false
387 } else {
388 async_modules.contains(&ResolvedVc::upcast(client_reference_module))
389 };
390
391 entry_manifest.client_modules.module_exports.insert(
392 get_client_reference_module_key(&server_path, "*"),
393 ManifestNodeEntry {
394 name: rcstr!("*"),
395 id: (&client_chunk_item_id).into(),
396 chunks: client_chunks_paths,
397 r#async: client_is_async || ssr_is_async,
402 },
403 );
404
405 let mut ssr_manifest_node = ManifestNode::default();
406 ssr_manifest_node.module_exports.insert(
407 rcstr!("*"),
408 ManifestNodeEntry {
409 name: rcstr!("*"),
410 id: (&ssr_chunk_item_id).into(),
411 chunks: ssr_chunks_paths
412 .into_iter()
413 .map(ClientChunk::Path)
414 .collect(),
415 r#async: client_is_async || ssr_is_async,
417 },
418 );
419
420 let mut rsc_manifest_node = ManifestNode::default();
421 rsc_manifest_node.module_exports.insert(
422 rcstr!("*"),
423 ManifestNodeEntry {
424 name: rcstr!("*"),
425 id: (&rsc_chunk_item_id).into(),
426 chunks: vec![],
427 r#async: rsc_is_async,
428 },
429 );
430
431 match runtime {
432 NextRuntime::NodeJs => {
433 entry_manifest
434 .ssr_module_mapping
435 .insert((&client_chunk_item_id).into(), ssr_manifest_node);
436 entry_manifest
437 .rsc_module_mapping
438 .insert((&client_chunk_item_id).into(), rsc_manifest_node);
439 }
440 NextRuntime::Edge => {
441 entry_manifest
442 .edge_ssr_module_mapping
443 .insert((&client_chunk_item_id).into(), ssr_manifest_node);
444 entry_manifest
445 .edge_rsc_module_mapping
446 .insert((&client_chunk_item_id).into(), rsc_manifest_node);
447 }
448 }
449 }
450 }
451
452 for (server_component, client_assets) in layout_segment_client_chunks.iter() {
454 let server_component_name = server_component
459 .source_path()
460 .await?
461 .with_extension("")
462 .to_string_ref()
463 .await?;
464 let entry_js_files = entry_manifest
465 .entry_js_files
466 .entry(server_component_name.clone())
467 .or_default();
468 let entry_css_files = entry_manifest
469 .entry_css_files
470 .entry(server_component_name)
471 .or_default();
472
473 let client_chunks = client_assets.primary_assets().await?;
474 let client_chunks_with_path =
475 cached_chunk_paths(&mut client_chunk_path_cache, client_chunks.iter().copied())
476 .await?;
477 let inlined_css = *next_config.inline_css().await? && mode.is_production();
479 let generate_component_chunks =
483 *next_config.turbopack_generate_component_chunks().await?;
484
485 for (chunk, chunk_path) in client_chunks_with_path {
486 if let Some(path) = client_relative_path.get_path_to(&chunk_path) {
487 let path = path.into();
490 if chunk_path.has_extension(".css") {
491 let content = if inlined_css {
492 Some(
493 if let Some(content_file) =
494 chunk.content().file_content().await?.as_content()
495 {
496 content_file.content().to_str()?.into()
497 } else {
498 RcStr::default()
499 },
500 )
501 } else {
502 None
503 };
504 entry_css_files.insert(CssResource {
505 path,
506 inlined: inlined_css,
507 content,
508 });
509 } else if !mode.is_production()
510 || !generate_component_chunks
511 || !client_reference_chunk_paths.contains(&path)
512 {
513 entry_js_files.insert(path);
514 }
515 }
516 }
517 }
518
519 let client_reference_manifest_json = serde_json::to_string(&entry_manifest).unwrap();
520
521 let normalized_manifest_entry = entry_name.replace("%5F", "_");
527 Ok(ClientReferenceManifestResult {
528 content: AssetContent::file(
529 FileContent::Content(File::from(formatdoc! {
530 r#"
531 globalThis.__RSC_MANIFEST = globalThis.__RSC_MANIFEST || {{}};
532 globalThis.__RSC_MANIFEST[{entry_name}] = {manifest};
533 {suffix}
534 "#,
535 entry_name = StringifyJs(&normalized_manifest_entry),
536 manifest = &client_reference_manifest_json,
537 suffix = if add_deployment_id_at_runtime {
538 formatdoc!{
539 r#"
540 for (const key in globalThis.__RSC_MANIFEST[{entry_name}].clientModules) {{
541 const val = {{ ...globalThis.__RSC_MANIFEST[{entry_name}].clientModules[key] }}
542 globalThis.__RSC_MANIFEST[{entry_name}].clientModules[key] = val
543 val.chunks = val.chunks.map((c) =>
544 typeof c === 'string'
545 ? `${{c}}?dpl=${{process.env.NEXT_DEPLOYMENT_ID}}`
546 : [`${{c[0]}}?dpl=${{process.env.NEXT_DEPLOYMENT_ID}}`, c[1], c[2]])
547 }}
548 "#,
549 entry_name = StringifyJs(&normalized_manifest_entry),
550 }
551 } else {
552 "".to_string()
553 }
554 }))
555 .cell(),
556 )
557 .to_resolved()
558 .await?,
559 references: ResolvedVc::cell(references.into_iter().collect()),
560 }
561 .cell())
562 }
563 .instrument(span)
564 .await
565}
566
567impl From<&TurbopackModuleId> for ModuleId {
568 fn from(module_id: &TurbopackModuleId) -> Self {
569 match module_id {
570 TurbopackModuleId::String(string) => ModuleId::String(string.clone()),
571 TurbopackModuleId::Number(number) => ModuleId::Number(*number as _),
572 }
573 }
574}
575
576async fn client_chunk_components(
577 chunk: ResolvedVc<Box<dyn OutputAsset>>,
578 client_relative_path: &FileSystemPath,
579) -> Result<Vec<(RcStr, u64)>> {
580 let Some(output_chunk) = ResolvedVc::try_sidecast::<Box<dyn OutputChunk>>(chunk) else {
581 return Ok(Vec::new());
582 };
583 let Some(component_chunks) = output_chunk.runtime_info().await?.module_chunks else {
584 return Ok(Vec::new());
585 };
586 let component_assets = component_chunks.await?;
587 let mut components = Vec::with_capacity(component_assets.len());
588 for component in component_assets.iter() {
589 let component_path = component.path().await?;
590 if let Some(rel) = client_relative_path.get_path_to(&component_path)
591 && rel.ends_with(".js")
592 {
593 let size = component
594 .content()
595 .file_content()
596 .await?
597 .as_content()
598 .map_or(0, |file| file.content().len() as u64);
599 components.push((RcStr::from(rel), size));
600 }
601 }
602 Ok(components)
603}
604
605pub fn get_client_reference_module_key(server_path: &str, export_name: &str) -> RcStr {
607 if export_name == "*" {
608 server_path.into()
609 } else {
610 format!("{server_path}#{export_name}").into()
611 }
612}