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