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 chunk_suffix_path = next_config.chunk_suffix_path().owned().await?;
171 let prefix_path = next_config.computed_asset_prefix().owned().await?;
172 let suffix_path = chunk_suffix_path.unwrap_or_default();
173
174 entry_manifest.module_loading.cross_origin = next_config.cross_origin().owned().await?;
178 let ClientReferencesChunks {
179 client_component_client_chunks,
180 layout_segment_client_chunks,
181 client_component_ssr_chunks,
182 } = &*client_references_chunks.await?;
183 let client_relative_path = client_relative_path.clone();
184 let node_root_ref = node_root.clone();
185
186 let client_references_ecmascript = client_references
187 .await?
188 .client_references
189 .iter()
190 .map(async |r| {
191 Ok(match r.ty {
192 ClientReferenceType::EcmascriptClientReference(r) => Some((r, r.await?)),
193 ClientReferenceType::CssClientReference(_) => None,
194 })
195 })
196 .try_flat_join()
197 .await?;
198
199 let async_modules = async_module_info
200 .is_async_multiple(Vc::cell(
201 client_references_ecmascript
202 .iter()
203 .flat_map(|(r, r_val)| {
204 [
205 ResolvedVc::upcast(*r),
206 ResolvedVc::upcast(r_val.client_module),
207 ResolvedVc::upcast(r_val.ssr_module),
208 ]
209 })
210 .collect(),
211 ))
212 .await?;
213
214 async fn cached_chunk_paths(
215 cache: &mut FxHashMap<ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath>,
216 chunks: impl Iterator<Item = ResolvedVc<Box<dyn OutputAsset>>>,
217 ) -> Result<impl Iterator<Item = (ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath)>>
218 {
219 let results = chunks
220 .into_iter()
221 .map(|chunk| (chunk, cache.get(&chunk).cloned()))
222 .map(async |(chunk, path)| {
223 Ok(if let Some(path) = path {
224 (chunk, Either::Left(path))
225 } else {
226 (chunk, Either::Right(chunk.path().owned().await?))
227 })
228 })
229 .try_join()
230 .await?;
231
232 for (chunk, path) in &results {
233 if let Either::Right(path) = path {
234 cache.insert(*chunk, path.clone());
235 }
236 }
237 Ok(results.into_iter().map(|(chunk, path)| match path {
238 Either::Left(path) => (chunk, path),
239 Either::Right(path) => (chunk, path),
240 }))
241 }
242 let mut client_chunk_path_cache: FxHashMap<
243 ResolvedVc<Box<dyn OutputAsset>>,
244 FileSystemPath,
245 > = FxHashMap::default();
246 let mut ssr_chunk_path_cache: FxHashMap<ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath> =
247 FxHashMap::default();
248
249 for (client_reference_module, client_reference_module_ref) in client_references_ecmascript {
250 let app_client_reference_ty =
251 ClientReferenceType::EcmascriptClientReference(client_reference_module);
252
253 let server_path = client_reference_module_ref.server_ident.to_string().await?;
254 let client_module = client_reference_module_ref.client_module;
255 let client_chunk_item_id = client_module
256 .chunk_item_id(**client_chunking_context)
257 .await?;
258
259 let (client_chunks_paths, client_is_async) = if let Some(client_assets) =
260 client_component_client_chunks.get(&app_client_reference_ty)
261 {
262 let client_chunks = client_assets.primary_assets().await?;
263 let client_referenced_assets = client_assets.referenced_assets().await?;
264 references.extend(client_chunks.iter());
265 references.extend(client_referenced_assets.iter());
266
267 let client_chunks_paths =
268 cached_chunk_paths(&mut client_chunk_path_cache, client_chunks.iter().copied())
269 .await?;
270
271 let chunk_paths = client_chunks_paths
272 .filter_map(|(_, chunk_path)| {
273 client_relative_path
274 .get_path_to(&chunk_path)
275 .map(ToString::to_string)
276 })
277 .filter(|path| path.ends_with(".js"))
280 .map(|path| {
281 format!(
282 "{}{}{}",
283 prefix_path,
284 path.split('/').map(encode_uri_component).format("/"),
285 suffix_path
286 )
287 })
288 .map(RcStr::from)
289 .collect::<Vec<_>>();
290
291 let is_async = async_modules.contains(&ResolvedVc::upcast(client_module));
292
293 (chunk_paths, is_async)
294 } else {
295 (Vec::new(), false)
296 };
297
298 if let Some(ssr_chunking_context) = *ssr_chunking_context {
299 let ssr_module = client_reference_module_ref.ssr_module;
300 let ssr_chunk_item_id = ssr_module.chunk_item_id(*ssr_chunking_context).await?;
301
302 let rsc_chunk_item_id = client_reference_module
303 .chunk_item_id(*ssr_chunking_context)
304 .await?;
305
306 let (ssr_chunks_paths, ssr_is_async) = if *runtime == NextRuntime::Edge {
307 (Vec::new(), false)
312 } else if let Some(ssr_assets) =
313 client_component_ssr_chunks.get(&app_client_reference_ty)
314 {
315 let ssr_chunks = ssr_assets.primary_assets().await?;
316 let ssr_referenced_assets = ssr_assets.referenced_assets().await?;
317 references.extend(ssr_chunks.iter());
318 references.extend(ssr_referenced_assets.iter());
319
320 let ssr_chunks_paths =
321 cached_chunk_paths(&mut ssr_chunk_path_cache, ssr_chunks.iter().copied())
322 .await?;
323 let chunk_paths = ssr_chunks_paths
324 .filter_map(|(_, chunk_path)| {
325 node_root_ref
326 .get_path_to(&chunk_path)
327 .map(ToString::to_string)
328 })
329 .map(RcStr::from)
330 .collect::<Vec<_>>();
331
332 let is_async = async_modules.contains(&ResolvedVc::upcast(ssr_module));
333
334 (chunk_paths, is_async)
335 } else {
336 (Vec::new(), false)
337 };
338
339 let rsc_is_async = if *runtime == NextRuntime::Edge {
340 false
341 } else {
342 async_modules.contains(&ResolvedVc::upcast(client_reference_module))
343 };
344
345 entry_manifest.client_modules.module_exports.insert(
346 get_client_reference_module_key(&server_path, "*"),
347 ManifestNodeEntry {
348 name: rcstr!("*"),
349 id: (&*client_chunk_item_id).into(),
350 chunks: client_chunks_paths,
351 r#async: client_is_async || ssr_is_async,
356 },
357 );
358
359 let mut ssr_manifest_node = ManifestNode::default();
360 ssr_manifest_node.module_exports.insert(
361 rcstr!("*"),
362 ManifestNodeEntry {
363 name: rcstr!("*"),
364 id: (&*ssr_chunk_item_id).into(),
365 chunks: ssr_chunks_paths,
366 r#async: client_is_async || ssr_is_async,
368 },
369 );
370
371 let mut rsc_manifest_node = ManifestNode::default();
372 rsc_manifest_node.module_exports.insert(
373 rcstr!("*"),
374 ManifestNodeEntry {
375 name: rcstr!("*"),
376 id: (&*rsc_chunk_item_id).into(),
377 chunks: vec![],
378 r#async: rsc_is_async,
379 },
380 );
381
382 match runtime {
383 NextRuntime::NodeJs => {
384 entry_manifest
385 .ssr_module_mapping
386 .insert((&*client_chunk_item_id).into(), ssr_manifest_node);
387 entry_manifest
388 .rsc_module_mapping
389 .insert((&*client_chunk_item_id).into(), rsc_manifest_node);
390 }
391 NextRuntime::Edge => {
392 entry_manifest
393 .edge_ssr_module_mapping
394 .insert((&*client_chunk_item_id).into(), ssr_manifest_node);
395 entry_manifest
396 .edge_rsc_module_mapping
397 .insert((&*client_chunk_item_id).into(), rsc_manifest_node);
398 }
399 }
400 }
401 }
402
403 for (server_component, client_assets) in layout_segment_client_chunks.iter() {
405 let server_component_name = server_component
406 .server_path()
407 .await?
408 .with_extension("")
409 .value_to_string()
410 .owned()
411 .await?;
412 let entry_js_files = entry_manifest
413 .entry_js_files
414 .entry(server_component_name.clone())
415 .or_default();
416 let entry_css_files = entry_manifest
417 .entry_css_files
418 .entry(server_component_name)
419 .or_default();
420
421 let client_chunks = client_assets.primary_assets().await?;
422 let client_chunks_with_path =
423 cached_chunk_paths(&mut client_chunk_path_cache, client_chunks.iter().copied())
424 .await?;
425 let inlined_css = *next_config.inline_css().await? && mode.is_production();
427
428 for (chunk, chunk_path) in client_chunks_with_path {
429 if let Some(path) = client_relative_path.get_path_to(&chunk_path) {
430 let path = path.into();
433 if chunk_path.has_extension(".css") {
434 let content = if inlined_css {
435 Some(
436 if let Some(content_file) =
437 chunk.content().file_content().await?.as_content()
438 {
439 content_file.content().to_str()?.into()
440 } else {
441 RcStr::default()
442 },
443 )
444 } else {
445 None
446 };
447 entry_css_files.insert(CssResource {
448 path,
449 inlined: inlined_css,
450 content,
451 });
452 } else {
453 entry_js_files.insert(path);
454 }
455 }
456 }
457 }
458
459 let client_reference_manifest_json = serde_json::to_string(&entry_manifest).unwrap();
460
461 let normalized_manifest_entry = entry_name.replace("%5F", "_");
467 Ok(ClientReferenceManifestResult {
468 content: AssetContent::file(
469 FileContent::Content(File::from(formatdoc! {
470 r#"
471 globalThis.__RSC_MANIFEST = globalThis.__RSC_MANIFEST || {{}};
472 globalThis.__RSC_MANIFEST[{entry_name}] = {manifest}
473 "#,
474 entry_name = StringifyJs(&normalized_manifest_entry),
475 manifest = &client_reference_manifest_json
476 }))
477 .cell(),
478 )
479 .to_resolved()
480 .await?,
481 references: ResolvedVc::cell(references.into_iter().collect()),
482 }
483 .cell())
484 }
485 .instrument(span)
486 .await
487}
488
489impl From<&TurbopackModuleId> for ModuleId {
490 fn from(module_id: &TurbopackModuleId) -> Self {
491 match module_id {
492 TurbopackModuleId::String(string) => ModuleId::String(string.clone()),
493 TurbopackModuleId::Number(number) => ModuleId::Number(*number as _),
494 }
495 }
496}
497
498pub fn get_client_reference_module_key(server_path: &str, export_name: &str) -> RcStr {
500 if export_name == "*" {
501 server_path.into()
502 } else {
503 format!("{server_path}#{export_name}").into()
504 }
505}