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