1use anyhow::Result;
2use either::Either;
3use indoc::formatdoc;
4use itertools::Itertools;
5use rustc_hash::FxHashMap;
6use serde::Serialize;
7use tracing::Instrument;
8use turbo_rcstr::{RcStr, rcstr};
9use turbo_tasks::{
10 FxIndexMap, FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToString, Vc,
11};
12use turbo_tasks_fs::{File, FileSystemPath};
13use turbopack_core::{
14 asset::{Asset, AssetContent},
15 chunk::{
16 ChunkGroupResult, ChunkingContext, ModuleChunkItemIdExt, ModuleId as TurbopackModuleId,
17 },
18 module_graph::async_module_info::AsyncModulesInfo,
19 output::{OutputAsset, OutputAssets, OutputAssetsWithReferenced},
20};
21use turbopack_ecmascript::utils::StringifyJs;
22
23use crate::{
24 mode::NextMode,
25 next_app::ClientReferencesChunks,
26 next_client_reference::{ClientReferenceGraphResult, ClientReferenceType},
27 next_config::{CrossOriginConfig, NextConfig},
28 next_manifests::{ModuleId, encode_uri_component::encode_uri_component},
29 util::NextRuntime,
30};
31
32#[derive(Serialize, Default, Debug)]
33#[serde(rename_all = "camelCase")]
34pub struct SerializedClientReferenceManifest {
35 pub module_loading: ModuleLoading,
36 pub client_modules: ManifestNode,
39 pub ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
42 #[serde(rename = "edgeSSRModuleMapping")]
44 pub edge_ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
45 pub rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
48 #[serde(rename = "edgeRscModuleMapping")]
50 pub edge_rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
51 #[serde(rename = "entryCSSFiles")]
53 pub entry_css_files: FxIndexMap<RcStr, FxIndexSet<CssResource>>,
54 #[serde(rename = "entryJSFiles")]
56 pub entry_js_files: FxIndexMap<RcStr, FxIndexSet<RcStr>>,
57}
58
59#[derive(Serialize, Debug, Clone, Eq, Hash, PartialEq)]
60pub struct CssResource {
61 pub path: RcStr,
62 pub inlined: bool,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub content: Option<RcStr>,
65}
66
67#[derive(Serialize, Default, Debug)]
68#[serde(rename_all = "camelCase")]
69pub struct ModuleLoading {
70 pub prefix: RcStr,
71 pub cross_origin: Option<CrossOriginConfig>,
72}
73
74#[derive(Serialize, Default, Debug, Clone)]
75#[serde(rename_all = "camelCase")]
76pub struct ManifestNode {
77 #[serde(flatten)]
79 pub module_exports: FxIndexMap<RcStr, ManifestNodeEntry>,
80}
81
82#[derive(Serialize, Debug, Clone)]
83#[serde(rename_all = "camelCase")]
84pub struct ManifestNodeEntry {
85 pub id: ModuleId,
87 pub name: RcStr,
89 pub chunks: Vec<RcStr>,
91 pub r#async: bool,
93}
94
95#[turbo_tasks::value(shared)]
96pub struct ClientReferenceManifest {
97 pub node_root: FileSystemPath,
98 pub client_relative_path: FileSystemPath,
99 pub entry_name: RcStr,
100 pub client_references: ResolvedVc<ClientReferenceGraphResult>,
101 pub client_references_chunks: ResolvedVc<ClientReferencesChunks>,
102 pub client_chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
103 pub ssr_chunking_context: Option<ResolvedVc<Box<dyn ChunkingContext>>>,
104 pub async_module_info: ResolvedVc<AsyncModulesInfo>,
105 pub next_config: ResolvedVc<NextConfig>,
106 pub runtime: NextRuntime,
107 pub mode: NextMode,
108}
109
110#[turbo_tasks::value_impl]
111impl OutputAsset for ClientReferenceManifest {
112 #[turbo_tasks::function]
113 async fn path(&self) -> Result<Vc<FileSystemPath>> {
114 let normalized_manifest_entry = self.entry_name.replace("%5F", "_");
115 Ok(self
116 .node_root
117 .join(&format!(
118 "server/app{normalized_manifest_entry}_client-reference-manifest.js",
119 ))?
120 .cell())
121 }
122
123 #[turbo_tasks::function]
124 async fn references(self: Vc<Self>) -> Result<Vc<OutputAssets>> {
125 Ok(*build_manifest(self).await?.references)
126 }
127}
128
129#[turbo_tasks::value_impl]
130impl Asset for ClientReferenceManifest {
131 #[turbo_tasks::function]
132 async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
133 Ok(*build_manifest(self).await?.content)
134 }
135}
136
137#[turbo_tasks::value(shared)]
138struct ClientReferenceManifestResult {
139 content: ResolvedVc<AssetContent>,
140 references: ResolvedVc<OutputAssets>,
141}
142
143#[turbo_tasks::function]
144async fn build_manifest(
145 manifest: Vc<ClientReferenceManifest>,
146) -> Result<Vc<ClientReferenceManifestResult>> {
147 let ClientReferenceManifest {
148 node_root,
149 client_relative_path,
150 entry_name,
151 client_references,
152 client_references_chunks,
153 client_chunking_context,
154 ssr_chunking_context,
155 async_module_info,
156 next_config,
157 runtime,
158 mode,
159 } = &*manifest.await?;
160 let span = tracing::info_span!(
161 "build client reference manifest",
162 entry_name = display(&entry_name)
163 );
164 async move {
165 let mut entry_manifest: SerializedClientReferenceManifest = Default::default();
166 let mut references = FxIndexSet::default();
167 let chunk_suffix_path = next_config.chunk_suffix_path().owned().await?;
168 let prefix_path = next_config.computed_asset_prefix().owned().await?;
169 let suffix_path = chunk_suffix_path.unwrap_or_default();
170
171 entry_manifest.module_loading.cross_origin = next_config.cross_origin().owned().await?;
175 let ClientReferencesChunks {
176 client_component_client_chunks,
177 layout_segment_client_chunks,
178 client_component_ssr_chunks,
179 } = &*client_references_chunks.await?;
180 let client_relative_path = client_relative_path.clone();
181 let node_root_ref = node_root.clone();
182
183 let client_references_ecmascript = client_references
184 .await?
185 .client_references
186 .iter()
187 .map(async |r| {
188 Ok(match r.ty() {
189 ClientReferenceType::EcmascriptClientReference(r) => Some((r, r.await?)),
190 ClientReferenceType::CssClientReference(_) => None,
191 })
192 })
193 .try_flat_join()
194 .await?;
195
196 let async_modules = async_module_info
197 .is_async_multiple(Vc::cell(
198 client_references_ecmascript
199 .iter()
200 .flat_map(|(r, r_val)| {
201 [
202 ResolvedVc::upcast(*r),
203 ResolvedVc::upcast(r_val.client_module),
204 ResolvedVc::upcast(r_val.ssr_module),
205 ]
206 })
207 .collect(),
208 ))
209 .await?;
210
211 async fn cached_chunk_paths(
212 cache: &mut FxHashMap<ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath>,
213 chunks: impl Iterator<Item = ResolvedVc<Box<dyn OutputAsset>>>,
214 ) -> Result<impl Iterator<Item = (ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath)>>
215 {
216 let results = chunks
217 .into_iter()
218 .map(|chunk| (chunk, cache.get(&chunk).cloned()))
219 .map(async |(chunk, path)| {
220 Ok(if let Some(path) = path {
221 (chunk, Either::Left(path))
222 } else {
223 (chunk, Either::Right(chunk.path().owned().await?))
224 })
225 })
226 .try_join()
227 .await?;
228
229 for (chunk, path) in &results {
230 if let Either::Right(path) = path {
231 cache.insert(*chunk, path.clone());
232 }
233 }
234 Ok(results.into_iter().map(|(chunk, path)| match path {
235 Either::Left(path) => (chunk, path),
236 Either::Right(path) => (chunk, path),
237 }))
238 }
239 let mut client_chunk_path_cache: FxHashMap<
240 ResolvedVc<Box<dyn OutputAsset>>,
241 FileSystemPath,
242 > = FxHashMap::default();
243 let mut ssr_chunk_path_cache: FxHashMap<ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath> =
244 FxHashMap::default();
245
246 for (client_reference_module, client_reference_module_ref) in client_references_ecmascript {
247 let app_client_reference_ty =
248 ClientReferenceType::EcmascriptClientReference(client_reference_module);
249
250 let server_path = client_reference_module_ref.server_ident.to_string().await?;
251 let client_module = client_reference_module_ref.client_module;
252 let client_chunk_item_id = client_module
253 .chunk_item_id(**client_chunking_context)
254 .await?;
255
256 let (client_chunks_paths, client_is_async) = if let Some(ChunkGroupResult {
257 assets: client_chunks,
258 referenced_assets: client_referenced_assets,
259 availability_info: _,
260 }) =
261 client_component_client_chunks.get(&app_client_reference_ty)
262 {
263 let client_chunks = client_chunks.await?;
264 let client_referenced_assets = client_referenced_assets.await?;
265 references.extend(client_chunks.iter());
266 references.extend(client_referenced_assets.iter());
267
268 let client_chunks_paths =
269 cached_chunk_paths(&mut client_chunk_path_cache, client_chunks.iter().copied())
270 .await?;
271
272 let chunk_paths = client_chunks_paths
273 .filter_map(|(_, chunk_path)| {
274 client_relative_path
275 .get_path_to(&chunk_path)
276 .map(ToString::to_string)
277 })
278 .filter(|path| path.ends_with(".js"))
281 .map(|path| {
282 format!(
283 "{}{}{}",
284 prefix_path,
285 path.split('/').map(encode_uri_component).format("/"),
286 suffix_path
287 )
288 })
289 .map(RcStr::from)
290 .collect::<Vec<_>>();
291
292 let is_async = async_modules.contains(&ResolvedVc::upcast(client_module));
293
294 (chunk_paths, is_async)
295 } else {
296 (Vec::new(), false)
297 };
298
299 if let Some(ssr_chunking_context) = *ssr_chunking_context {
300 let ssr_module = client_reference_module_ref.ssr_module;
301 let ssr_chunk_item_id = ssr_module.chunk_item_id(*ssr_chunking_context).await?;
302
303 let rsc_chunk_item_id = client_reference_module
304 .chunk_item_id(*ssr_chunking_context)
305 .await?;
306
307 let (ssr_chunks_paths, ssr_is_async) = if *runtime == NextRuntime::Edge {
308 (Vec::new(), false)
313 } else if let Some(ChunkGroupResult {
314 assets: ssr_chunks,
315 referenced_assets: ssr_referenced_assets,
316 availability_info: _,
317 }) = client_component_ssr_chunks.get(&app_client_reference_ty)
318 {
319 let ssr_chunks = ssr_chunks.await?;
320 let ssr_referenced_assets = ssr_referenced_assets.await?;
321 references.extend(ssr_chunks.iter());
322 references.extend(ssr_referenced_assets.iter());
323
324 let ssr_chunks_paths =
325 cached_chunk_paths(&mut ssr_chunk_path_cache, ssr_chunks.iter().copied())
326 .await?;
327 let chunk_paths = ssr_chunks_paths
328 .filter_map(|(_, chunk_path)| {
329 node_root_ref
330 .get_path_to(&chunk_path)
331 .map(ToString::to_string)
332 })
333 .map(RcStr::from)
334 .collect::<Vec<_>>();
335
336 let is_async = async_modules.contains(&ResolvedVc::upcast(ssr_module));
337
338 (chunk_paths, is_async)
339 } else {
340 (Vec::new(), false)
341 };
342
343 let rsc_is_async = if *runtime == NextRuntime::Edge {
344 false
345 } else {
346 async_modules.contains(&ResolvedVc::upcast(client_reference_module))
347 };
348
349 entry_manifest.client_modules.module_exports.insert(
350 get_client_reference_module_key(&server_path, "*"),
351 ManifestNodeEntry {
352 name: rcstr!("*"),
353 id: (&*client_chunk_item_id).into(),
354 chunks: client_chunks_paths,
355 r#async: client_is_async || ssr_is_async,
360 },
361 );
362
363 let mut ssr_manifest_node = ManifestNode::default();
364 ssr_manifest_node.module_exports.insert(
365 rcstr!("*"),
366 ManifestNodeEntry {
367 name: rcstr!("*"),
368 id: (&*ssr_chunk_item_id).into(),
369 chunks: ssr_chunks_paths,
370 r#async: client_is_async || ssr_is_async,
372 },
373 );
374
375 let mut rsc_manifest_node = ManifestNode::default();
376 rsc_manifest_node.module_exports.insert(
377 rcstr!("*"),
378 ManifestNodeEntry {
379 name: rcstr!("*"),
380 id: (&*rsc_chunk_item_id).into(),
381 chunks: vec![],
382 r#async: rsc_is_async,
383 },
384 );
385
386 match runtime {
387 NextRuntime::NodeJs => {
388 entry_manifest
389 .ssr_module_mapping
390 .insert((&*client_chunk_item_id).into(), ssr_manifest_node);
391 entry_manifest
392 .rsc_module_mapping
393 .insert((&*client_chunk_item_id).into(), rsc_manifest_node);
394 }
395 NextRuntime::Edge => {
396 entry_manifest
397 .edge_ssr_module_mapping
398 .insert((&*client_chunk_item_id).into(), ssr_manifest_node);
399 entry_manifest
400 .edge_rsc_module_mapping
401 .insert((&*client_chunk_item_id).into(), rsc_manifest_node);
402 }
403 }
404 }
405 }
406
407 for (
409 server_component,
410 OutputAssetsWithReferenced {
411 assets: client_chunks,
412 referenced_assets: _,
413 },
414 ) in layout_segment_client_chunks.iter()
415 {
416 let server_component_name = server_component
417 .server_path()
418 .await?
419 .with_extension("")
420 .value_to_string()
421 .owned()
422 .await?;
423 let entry_js_files = entry_manifest
424 .entry_js_files
425 .entry(server_component_name.clone())
426 .or_default();
427 let entry_css_files = entry_manifest
428 .entry_css_files
429 .entry(server_component_name)
430 .or_default();
431
432 let client_chunks = &client_chunks.await?;
433 let client_chunks_with_path =
434 cached_chunk_paths(&mut client_chunk_path_cache, client_chunks.iter().copied())
435 .await?;
436 let inlined_css = *next_config.inline_css().await? && mode.is_production();
438
439 for (chunk, chunk_path) in client_chunks_with_path {
440 if let Some(path) = client_relative_path.get_path_to(&chunk_path) {
441 let path = path.into();
444 if chunk_path.has_extension(".css") {
445 let content = if inlined_css {
446 Some(
447 if let Some(content_file) =
448 chunk.content().file_content().await?.as_content()
449 {
450 content_file.content().to_str()?.into()
451 } else {
452 RcStr::default()
453 },
454 )
455 } else {
456 None
457 };
458 entry_css_files.insert(CssResource {
459 path,
460 inlined: inlined_css,
461 content,
462 });
463 } else {
464 entry_js_files.insert(path);
465 }
466 }
467 }
468 }
469
470 let client_reference_manifest_json = serde_json::to_string(&entry_manifest).unwrap();
471
472 let normalized_manifest_entry = entry_name.replace("%5F", "_");
478 Ok(ClientReferenceManifestResult {
479 content: AssetContent::file(
480 File::from(formatdoc! {
481 r#"
482 globalThis.__RSC_MANIFEST = globalThis.__RSC_MANIFEST || {{}};
483 globalThis.__RSC_MANIFEST[{entry_name}] = {manifest}
484 "#,
485 entry_name = StringifyJs(&normalized_manifest_entry),
486 manifest = &client_reference_manifest_json
487 })
488 .into(),
489 )
490 .to_resolved()
491 .await?,
492 references: ResolvedVc::cell(references.into_iter().collect()),
493 }
494 .cell())
495 }
496 .instrument(span)
497 .await
498}
499
500impl From<&TurbopackModuleId> for ModuleId {
501 fn from(module_id: &TurbopackModuleId) -> Self {
502 match module_id {
503 TurbopackModuleId::String(string) => ModuleId::String(string.clone()),
504 TurbopackModuleId::Number(number) => ModuleId::Number(*number as _),
505 }
506 }
507}
508
509pub fn get_client_reference_module_key(server_path: &str, export_name: &str) -> RcStr {
511 if export_name == "*" {
512 server_path.into()
513 } else {
514 format!("{server_path}#{export_name}").into()
515 }
516}