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, FileContent, FileSystemPath};
13use turbopack_core::{
14 asset::{Asset, AssetContent},
15 chunk::{ChunkingContext, ModuleChunkItemIdExt, ModuleId as TurbopackModuleId},
16 module_graph::async_module_info::AsyncModulesInfo,
17 output::{OutputAsset, OutputAssets, OutputAssetsReference, OutputAssetsWithReferenced},
18};
19use turbopack_ecmascript::utils::StringifyJs;
20
21use crate::{
22 mode::NextMode,
23 next_app::ClientReferencesChunks,
24 next_client_reference::{ClientReferenceGraphResult, ClientReferenceType},
25 next_config::{CrossOriginConfig, NextConfig},
26 next_manifests::{ModuleId, encode_uri_component::encode_uri_component},
27 util::NextRuntime,
28};
29
30#[derive(Serialize, Default, Debug)]
31#[serde(rename_all = "camelCase")]
32pub struct SerializedClientReferenceManifest {
33 pub module_loading: ModuleLoading,
34 pub client_modules: ManifestNode,
37 pub ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
40 #[serde(rename = "edgeSSRModuleMapping")]
42 pub edge_ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
43 pub rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
46 #[serde(rename = "edgeRscModuleMapping")]
48 pub edge_rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
49 #[serde(rename = "entryCSSFiles")]
51 pub entry_css_files: FxIndexMap<RcStr, FxIndexSet<CssResource>>,
52 #[serde(rename = "entryJSFiles")]
54 pub entry_js_files: FxIndexMap<RcStr, FxIndexSet<RcStr>>,
55}
56
57#[derive(Serialize, Debug, Clone, Eq, Hash, PartialEq)]
58pub struct CssResource {
59 pub path: RcStr,
60 pub inlined: bool,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub content: Option<RcStr>,
63}
64
65#[derive(Serialize, Default, Debug)]
66#[serde(rename_all = "camelCase")]
67pub struct ModuleLoading {
68 pub prefix: RcStr,
69 pub cross_origin: Option<CrossOriginConfig>,
70}
71
72#[derive(Serialize, Default, Debug, Clone)]
73#[serde(rename_all = "camelCase")]
74pub struct ManifestNode {
75 #[serde(flatten)]
77 pub module_exports: FxIndexMap<RcStr, ManifestNodeEntry>,
78}
79
80#[derive(Serialize, Debug, Clone)]
81#[serde(rename_all = "camelCase")]
82pub struct ManifestNodeEntry {
83 pub id: ModuleId,
85 pub name: RcStr,
87 pub chunks: Vec<RcStr>,
89 pub r#async: bool,
91}
92
93#[turbo_tasks::value(shared)]
94pub struct ClientReferenceManifest {
95 pub node_root: FileSystemPath,
96 pub client_relative_path: FileSystemPath,
97 pub entry_name: RcStr,
98 pub client_references: ResolvedVc<ClientReferenceGraphResult>,
99 pub client_references_chunks: ResolvedVc<ClientReferencesChunks>,
100 pub client_chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
101 pub ssr_chunking_context: Option<ResolvedVc<Box<dyn ChunkingContext>>>,
102 pub async_module_info: ResolvedVc<AsyncModulesInfo>,
103 pub next_config: ResolvedVc<NextConfig>,
104 pub runtime: NextRuntime,
105 pub mode: NextMode,
106}
107
108#[turbo_tasks::value_impl]
109impl OutputAssetsReference for ClientReferenceManifest {
110 #[turbo_tasks::function]
111 async fn references(self: Vc<Self>) -> Result<Vc<OutputAssetsWithReferenced>> {
112 Ok(OutputAssetsWithReferenced::from_assets(
113 *build_manifest(self).await?.references,
114 ))
115 }
116}
117
118#[turbo_tasks::value_impl]
119impl OutputAsset for ClientReferenceManifest {
120 #[turbo_tasks::function]
121 async fn path(&self) -> Result<Vc<FileSystemPath>> {
122 let normalized_manifest_entry = self.entry_name.replace("%5F", "_");
123 Ok(self
124 .node_root
125 .join(&format!(
126 "server/app{normalized_manifest_entry}_client-reference-manifest.js",
127 ))?
128 .cell())
129 }
130}
131
132#[turbo_tasks::value_impl]
133impl Asset for ClientReferenceManifest {
134 #[turbo_tasks::function]
135 async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
136 Ok(*build_manifest(self).await?.content)
137 }
138}
139
140#[turbo_tasks::value(shared)]
141struct ClientReferenceManifestResult {
142 content: ResolvedVc<AssetContent>,
143 references: ResolvedVc<OutputAssets>,
144}
145
146#[turbo_tasks::function]
147async fn build_manifest(
148 manifest: Vc<ClientReferenceManifest>,
149) -> Result<Vc<ClientReferenceManifestResult>> {
150 let ClientReferenceManifest {
151 node_root,
152 client_relative_path,
153 entry_name,
154 client_references,
155 client_references_chunks,
156 client_chunking_context,
157 ssr_chunking_context,
158 async_module_info,
159 next_config,
160 runtime,
161 mode,
162 } = &*manifest.await?;
163 let span = tracing::info_span!(
164 "build client reference manifest",
165 entry_name = display(&entry_name)
166 );
167 async move {
168 let mut entry_manifest: SerializedClientReferenceManifest = Default::default();
169 let mut references = FxIndexSet::default();
170 let prefix_path = next_config.computed_asset_prefix().owned().await?;
171 let runtime_server_deployment_id_available =
172 *next_config.runtime_server_deployment_id_available().await?;
173 let suffix_path = if !runtime_server_deployment_id_available {
174 let asset_suffix_path = next_config.asset_suffix_path().owned().await?;
175 asset_suffix_path.unwrap_or_default()
176 } else {
177 rcstr!("")
178 };
179
180 entry_manifest.module_loading.cross_origin = next_config.cross_origin().owned().await?;
181 let ClientReferencesChunks {
182 client_component_client_chunks,
183 layout_segment_client_chunks,
184 client_component_ssr_chunks,
185 } = &*client_references_chunks.await?;
186 let client_relative_path = client_relative_path.clone();
187 let node_root_ref = node_root.clone();
188
189 let client_references_ecmascript = client_references
190 .await?
191 .client_references
192 .iter()
193 .map(async |r| {
194 Ok(match r.ty {
195 ClientReferenceType::EcmascriptClientReference(r) => Some((r, r.await?)),
196 ClientReferenceType::CssClientReference(_) => None,
197 })
198 })
199 .try_flat_join()
200 .await?;
201
202 let async_modules = client_references_ecmascript
203 .iter()
204 .flat_map(|(r, r_val)| {
205 [
206 ResolvedVc::upcast(*r),
207 ResolvedVc::upcast(r_val.client_module),
208 ResolvedVc::upcast(r_val.ssr_module),
209 ]
210 }).map(async move |asset| {
211 Ok(if async_module_info.is_async(asset).await? {
212 Some(asset)
213 } else {
214 None
215 })
216 }).try_flat_join().await?;
217
218 async fn cached_chunk_paths(
219 cache: &mut FxHashMap<ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath>,
220 chunks: impl Iterator<Item = ResolvedVc<Box<dyn OutputAsset>>>,
221 ) -> Result<impl Iterator<Item = (ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath)>>
222 {
223 let results = chunks
224 .into_iter()
225 .map(|chunk| (chunk, cache.get(&chunk).cloned()))
226 .map(async |(chunk, path)| {
227 Ok(if let Some(path) = path {
228 (chunk, Either::Left(path))
229 } else {
230 (chunk, Either::Right(chunk.path().owned().await?))
231 })
232 })
233 .try_join()
234 .await?;
235
236 for (chunk, path) in &results {
237 if let Either::Right(path) = path {
238 cache.insert(*chunk, path.clone());
239 }
240 }
241 Ok(results.into_iter().map(|(chunk, path)| match path {
242 Either::Left(path) => (chunk, path),
243 Either::Right(path) => (chunk, path),
244 }))
245 }
246 let mut client_chunk_path_cache: FxHashMap<
247 ResolvedVc<Box<dyn OutputAsset>>,
248 FileSystemPath,
249 > = FxHashMap::default();
250 let mut ssr_chunk_path_cache: FxHashMap<ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath> =
251 FxHashMap::default();
252
253 for (client_reference_module, client_reference_module_ref) in client_references_ecmascript {
254 let app_client_reference_ty =
255 ClientReferenceType::EcmascriptClientReference(client_reference_module);
256
257 let server_path = client_reference_module_ref.server_ident.to_string().await?;
258 let client_module = client_reference_module_ref.client_module;
259 let client_chunk_item_id = client_module
260 .chunk_item_id(**client_chunking_context)
261 .await?;
262
263 let (client_chunks_paths, client_is_async) = if let Some(client_assets) =
264 client_component_client_chunks.get(&app_client_reference_ty)
265 {
266 let client_chunks = client_assets.primary_assets().await?;
267 let client_referenced_assets = client_assets.referenced_assets().await?;
268 references.extend(client_chunks.iter());
269 references.extend(client_referenced_assets.iter());
270
271 let client_chunks_paths =
272 cached_chunk_paths(&mut client_chunk_path_cache, client_chunks.iter().copied())
273 .await?;
274
275 let chunk_paths = client_chunks_paths
276 .filter_map(|(_, chunk_path)| {
277 client_relative_path
278 .get_path_to(&chunk_path)
279 .map(ToString::to_string)
280 })
281 .filter(|path| path.ends_with(".js"))
284 .map(|path| {
285 format!(
286 "{}{}{}",
287 prefix_path,
288 path.split('/').map(encode_uri_component).format("/"),
289 suffix_path
290 )
291 })
292 .map(RcStr::from)
293 .collect::<Vec<_>>();
294
295 let is_async = async_modules.contains(&ResolvedVc::upcast(client_module));
296
297 (chunk_paths, is_async)
298 } else {
299 (Vec::new(), false)
300 };
301
302 if let Some(ssr_chunking_context) = *ssr_chunking_context {
303 let ssr_module = client_reference_module_ref.ssr_module;
304 let ssr_chunk_item_id = ssr_module.chunk_item_id(*ssr_chunking_context).await?;
305
306 let rsc_chunk_item_id = client_reference_module
307 .chunk_item_id(*ssr_chunking_context)
308 .await?;
309
310 let (ssr_chunks_paths, ssr_is_async) = if *runtime == NextRuntime::Edge {
311 (Vec::new(), false)
316 } else if let Some(ssr_assets) =
317 client_component_ssr_chunks.get(&app_client_reference_ty)
318 {
319 let ssr_chunks = ssr_assets.primary_assets().await?;
320 let ssr_referenced_assets = ssr_assets.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 (server_component, client_assets) in layout_segment_client_chunks.iter() {
409 let server_component_name = server_component
414 .source_path()
415 .await?
416 .with_extension("")
417 .value_to_string()
418 .owned()
419 .await?;
420 let entry_js_files = entry_manifest
421 .entry_js_files
422 .entry(server_component_name.clone())
423 .or_default();
424 let entry_css_files = entry_manifest
425 .entry_css_files
426 .entry(server_component_name)
427 .or_default();
428
429 let client_chunks = client_assets.primary_assets().await?;
430 let client_chunks_with_path =
431 cached_chunk_paths(&mut client_chunk_path_cache, client_chunks.iter().copied())
432 .await?;
433 let inlined_css = *next_config.inline_css().await? && mode.is_production();
435
436 for (chunk, chunk_path) in client_chunks_with_path {
437 if let Some(path) = client_relative_path.get_path_to(&chunk_path) {
438 let path = path.into();
441 if chunk_path.has_extension(".css") {
442 let content = if inlined_css {
443 Some(
444 if let Some(content_file) =
445 chunk.content().file_content().await?.as_content()
446 {
447 content_file.content().to_str()?.into()
448 } else {
449 RcStr::default()
450 },
451 )
452 } else {
453 None
454 };
455 entry_css_files.insert(CssResource {
456 path,
457 inlined: inlined_css,
458 content,
459 });
460 } else {
461 entry_js_files.insert(path);
462 }
463 }
464 }
465 }
466
467 let client_reference_manifest_json = serde_json::to_string(&entry_manifest).unwrap();
468
469 let normalized_manifest_entry = entry_name.replace("%5F", "_");
475 Ok(ClientReferenceManifestResult {
476 content: AssetContent::file(
477 FileContent::Content(File::from(formatdoc! {
478 r#"
479 globalThis.__RSC_MANIFEST = globalThis.__RSC_MANIFEST || {{}};
480 globalThis.__RSC_MANIFEST[{entry_name}] = {manifest};
481 {suffix}
482 "#,
483 entry_name = StringifyJs(&normalized_manifest_entry),
484 manifest = &client_reference_manifest_json,
485 suffix = if runtime_server_deployment_id_available {
486 formatdoc!{
487 r#"
488 for (const key in globalThis.__RSC_MANIFEST[{entry_name}].clientModules) {{
489 const val = {{ ...globalThis.__RSC_MANIFEST[{entry_name}].clientModules[key] }}
490 globalThis.__RSC_MANIFEST[{entry_name}].clientModules[key] = val
491 val.chunks = val.chunks.map((c) => `${{c}}?dpl=${{process.env.NEXT_DEPLOYMENT_ID}}`)
492 }}
493 "#,
494 entry_name = StringifyJs(&normalized_manifest_entry),
495 }
496 } else {
497 "".to_string()
498 }
499 }))
500 .cell(),
501 ).to_resolved().await?,
502 references: ResolvedVc::cell(references.into_iter().collect()),
503 }
504 .cell())
505 }
506 .instrument(span)
507 .await
508}
509
510impl From<&TurbopackModuleId> for ModuleId {
511 fn from(module_id: &TurbopackModuleId) -> Self {
512 match module_id {
513 TurbopackModuleId::String(string) => ModuleId::String(string.clone()),
514 TurbopackModuleId::Number(number) => ModuleId::Number(*number as _),
515 }
516 }
517}
518
519pub fn get_client_reference_module_key(server_path: &str, export_name: &str) -> RcStr {
521 if export_name == "*" {
522 server_path.into()
523 } else {
524 format!("{server_path}#{export_name}").into()
525 }
526}