next_core/next_manifests/
client_reference_manifest.rs1use anyhow::Result;
2use either::Either;
3use indoc::formatdoc;
4use itertools::Itertools;
5use rustc_hash::FxHashMap;
6use serde::{Deserialize, Serialize};
7use tracing::Instrument;
8use turbo_rcstr::RcStr;
9use turbo_tasks::{
10 FxIndexSet, ResolvedVc, TaskInput, TryFlatJoinIterExt, TryJoinIterExt, ValueToString, Vc,
11 trace::TraceRawVcs,
12};
13use turbo_tasks_fs::{File, FileSystemPath};
14use turbopack_core::{
15 asset::{Asset, AssetContent},
16 chunk::{ChunkingContext, ModuleChunkItemIdExt, ModuleId as TurbopackModuleId},
17 module_graph::async_module_info::AsyncModulesInfo,
18 output::OutputAsset,
19 virtual_output::VirtualOutputAsset,
20};
21use turbopack_ecmascript::utils::StringifyJs;
22
23use super::{ClientReferenceManifest, CssResource, ManifestNode, ManifestNodeEntry, ModuleId};
24use crate::{
25 mode::NextMode,
26 next_app::ClientReferencesChunks,
27 next_client_reference::{ClientReferenceGraphResult, ClientReferenceType},
28 next_config::NextConfig,
29 next_manifests::encode_uri_component::encode_uri_component,
30 util::NextRuntime,
31};
32
33#[derive(TaskInput, Clone, Hash, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)]
34pub struct ClientReferenceManifestOptions {
35 pub node_root: FileSystemPath,
36 pub client_relative_path: FileSystemPath,
37 pub entry_name: RcStr,
38 pub client_references: ResolvedVc<ClientReferenceGraphResult>,
39 pub client_references_chunks: ResolvedVc<ClientReferencesChunks>,
40 pub client_chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
41 pub ssr_chunking_context: Option<ResolvedVc<Box<dyn ChunkingContext>>>,
42 pub async_module_info: ResolvedVc<AsyncModulesInfo>,
43 pub next_config: ResolvedVc<NextConfig>,
44 pub runtime: NextRuntime,
45 pub mode: NextMode,
46}
47
48#[turbo_tasks::value_impl]
49impl ClientReferenceManifest {
50 #[turbo_tasks::function]
51 pub async fn build_output(
52 options: ClientReferenceManifestOptions,
53 ) -> Result<Vc<Box<dyn OutputAsset>>> {
54 let ClientReferenceManifestOptions {
55 node_root,
56 client_relative_path,
57 entry_name,
58 client_references,
59 client_references_chunks,
60 client_chunking_context,
61 ssr_chunking_context,
62 async_module_info,
63 next_config,
64 runtime,
65 mode,
66 } = options;
67 let span = tracing::info_span!(
68 "ClientReferenceManifest build output",
69 entry_name = display(&entry_name)
70 );
71 async move {
72 let mut entry_manifest: ClientReferenceManifest = Default::default();
73 let mut references = FxIndexSet::default();
74 let chunk_suffix_path = next_config.chunk_suffix_path().await?;
75 let prefix_path = next_config
76 .computed_asset_prefix()
77 .await?
78 .as_ref()
79 .map(|p| p.clone())
80 .unwrap_or_default();
81 let suffix_path = chunk_suffix_path
82 .as_ref()
83 .map(|p| p.to_string())
84 .unwrap_or("".into());
85
86 entry_manifest.module_loading.cross_origin = next_config
90 .await?
91 .cross_origin
92 .as_ref()
93 .map(|p| p.to_owned());
94 let ClientReferencesChunks {
95 client_component_client_chunks,
96 layout_segment_client_chunks,
97 client_component_ssr_chunks,
98 } = &*client_references_chunks.await?;
99 let client_relative_path = client_relative_path.clone();
100 let node_root_ref = node_root.clone();
101
102 let client_references_ecmascript = client_references
103 .await?
104 .client_references
105 .iter()
106 .map(async |r| {
107 Ok(match r.ty() {
108 ClientReferenceType::EcmascriptClientReference(r) => Some((r, r.await?)),
109 ClientReferenceType::CssClientReference(_) => None,
110 })
111 })
112 .try_flat_join()
113 .await?;
114
115 let async_modules = async_module_info
116 .is_async_multiple(Vc::cell(
117 client_references_ecmascript
118 .iter()
119 .flat_map(|(r, r_val)| {
120 [
121 ResolvedVc::upcast(*r),
122 ResolvedVc::upcast(r_val.client_module),
123 ResolvedVc::upcast(r_val.ssr_module),
124 ]
125 })
126 .collect(),
127 ))
128 .await?;
129
130 async fn cached_chunk_paths(
131 cache: &mut FxHashMap<ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath>,
132 chunks: impl Iterator<Item = ResolvedVc<Box<dyn OutputAsset>>>,
133 ) -> Result<impl Iterator<Item = (ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath)>>
134 {
135 let results = chunks
136 .into_iter()
137 .map(|chunk| (chunk, cache.get(&chunk).cloned()))
138 .map(async |(chunk, path)| {
139 Ok(if let Some(path) = path {
140 (chunk, Either::Left(path))
141 } else {
142 (chunk, Either::Right(chunk.path().await?.clone_value()))
143 })
144 })
145 .try_join()
146 .await?;
147
148 for (chunk, path) in &results {
149 if let Either::Right(path) = path {
150 cache.insert(*chunk, path.clone());
151 }
152 }
153 Ok(results.into_iter().map(|(chunk, path)| match path {
154 Either::Left(path) => (chunk, path),
155 Either::Right(path) => (chunk, path),
156 }))
157 }
158 let mut client_chunk_path_cache: FxHashMap<
159 ResolvedVc<Box<dyn OutputAsset>>,
160 FileSystemPath,
161 > = FxHashMap::default();
162 let mut ssr_chunk_path_cache: FxHashMap<
163 ResolvedVc<Box<dyn OutputAsset>>,
164 FileSystemPath,
165 > = FxHashMap::default();
166
167 for (client_reference_module, client_reference_module_ref) in
168 client_references_ecmascript
169 {
170 let app_client_reference_ty =
171 ClientReferenceType::EcmascriptClientReference(client_reference_module);
172
173 let server_path = client_reference_module_ref.server_ident.to_string().await?;
174 let client_module = client_reference_module_ref.client_module;
175 let client_chunk_item_id = client_module
176 .chunk_item_id(*ResolvedVc::upcast(client_chunking_context))
177 .await?;
178
179 let (client_chunks_paths, client_is_async) =
180 if let Some((client_chunks, _client_availability_info)) =
181 client_component_client_chunks.get(&app_client_reference_ty)
182 {
183 let client_chunks = client_chunks.await?;
184 references.extend(client_chunks.iter());
185 let client_chunks_paths = cached_chunk_paths(
186 &mut client_chunk_path_cache,
187 client_chunks.iter().copied(),
188 )
189 .await?;
190
191 let chunk_paths = client_chunks_paths
192 .filter_map(|(_, chunk_path)| {
193 client_relative_path
194 .get_path_to(&chunk_path)
195 .map(ToString::to_string)
196 })
197 .filter(|path| path.ends_with(".js"))
200 .map(|path| {
201 format!(
202 "{}{}{}",
203 prefix_path,
204 path.split('/').map(encode_uri_component).format("/"),
205 suffix_path
206 )
207 })
208 .map(RcStr::from)
209 .collect::<Vec<_>>();
210
211 let is_async = async_modules.contains(&ResolvedVc::upcast(client_module));
212
213 (chunk_paths, is_async)
214 } else {
215 (Vec::new(), false)
216 };
217
218 if let Some(ssr_chunking_context) = ssr_chunking_context {
219 let ssr_module = client_reference_module_ref.ssr_module;
220 let ssr_chunk_item_id = ssr_module
221 .chunk_item_id(*ResolvedVc::upcast(ssr_chunking_context))
222 .await?;
223
224 let rsc_chunk_item_id = client_reference_module
225 .chunk_item_id(*ResolvedVc::upcast(ssr_chunking_context))
226 .await?;
227
228 let (ssr_chunks_paths, ssr_is_async) = if runtime == NextRuntime::Edge {
229 (Vec::new(), false)
234 } else if let Some((ssr_chunks, _ssr_availability_info)) =
235 client_component_ssr_chunks.get(&app_client_reference_ty)
236 {
237 let ssr_chunks = ssr_chunks.await?;
238 references.extend(ssr_chunks.iter());
239
240 let ssr_chunks_paths = cached_chunk_paths(
241 &mut ssr_chunk_path_cache,
242 ssr_chunks.iter().copied(),
243 )
244 .await?;
245 let chunk_paths = ssr_chunks_paths
246 .filter_map(|(_, chunk_path)| {
247 node_root_ref
248 .get_path_to(&chunk_path)
249 .map(ToString::to_string)
250 })
251 .map(RcStr::from)
252 .collect::<Vec<_>>();
253
254 let is_async = async_modules.contains(&ResolvedVc::upcast(ssr_module));
255
256 (chunk_paths, is_async)
257 } else {
258 (Vec::new(), false)
259 };
260
261 let rsc_is_async = if runtime == NextRuntime::Edge {
262 false
263 } else {
264 async_modules.contains(&ResolvedVc::upcast(client_reference_module))
265 };
266
267 entry_manifest.client_modules.module_exports.insert(
268 get_client_reference_module_key(&server_path, "*"),
269 ManifestNodeEntry {
270 name: "*".into(),
271 id: (&*client_chunk_item_id).into(),
272 chunks: client_chunks_paths,
273 r#async: client_is_async || ssr_is_async,
278 },
279 );
280
281 let mut ssr_manifest_node = ManifestNode::default();
282 ssr_manifest_node.module_exports.insert(
283 "*".into(),
284 ManifestNodeEntry {
285 name: "*".into(),
286 id: (&*ssr_chunk_item_id).into(),
287 chunks: ssr_chunks_paths,
288 r#async: client_is_async || ssr_is_async,
290 },
291 );
292
293 let mut rsc_manifest_node = ManifestNode::default();
294 rsc_manifest_node.module_exports.insert(
295 "*".into(),
296 ManifestNodeEntry {
297 name: "*".into(),
298 id: (&*rsc_chunk_item_id).into(),
299 chunks: vec![],
300 r#async: rsc_is_async,
301 },
302 );
303
304 match runtime {
305 NextRuntime::NodeJs => {
306 entry_manifest
307 .ssr_module_mapping
308 .insert((&*client_chunk_item_id).into(), ssr_manifest_node);
309 entry_manifest
310 .rsc_module_mapping
311 .insert((&*client_chunk_item_id).into(), rsc_manifest_node);
312 }
313 NextRuntime::Edge => {
314 entry_manifest
315 .edge_ssr_module_mapping
316 .insert((&*client_chunk_item_id).into(), ssr_manifest_node);
317 entry_manifest
318 .edge_rsc_module_mapping
319 .insert((&*client_chunk_item_id).into(), rsc_manifest_node);
320 }
321 }
322 }
323 }
324
325 for (server_component, client_chunks) in layout_segment_client_chunks.iter() {
327 let server_component_name = server_component
328 .server_path()
329 .await?
330 .with_extension("")
331 .value_to_string()
332 .owned()
333 .await?;
334 let entry_js_files = entry_manifest
335 .entry_js_files
336 .entry(server_component_name.clone())
337 .or_default();
338 let entry_css_files = entry_manifest
339 .entry_css_files
340 .entry(server_component_name)
341 .or_default();
342
343 let client_chunks = &client_chunks.await?;
344 let client_chunks_with_path =
345 cached_chunk_paths(&mut client_chunk_path_cache, client_chunks.iter().copied())
346 .await?;
347 let inlined_css = next_config.await?.experimental.inline_css.unwrap_or(false)
349 && mode.is_production();
350
351 for (chunk, chunk_path) in client_chunks_with_path {
352 if let Some(path) = client_relative_path.get_path_to(&chunk_path) {
353 let path = path.into();
356 if chunk_path.has_extension(".css") {
357 let content = if inlined_css {
358 Some(
359 if let Some(content_file) =
360 chunk.content().file_content().await?.as_content()
361 {
362 content_file.content().to_str()?.into()
363 } else {
364 RcStr::default()
365 },
366 )
367 } else {
368 None
369 };
370 entry_css_files.insert(CssResource {
371 path,
372 inlined: inlined_css,
373 content,
374 });
375 } else {
376 entry_js_files.insert(path);
377 }
378 }
379 }
380 }
381
382 let client_reference_manifest_json = serde_json::to_string(&entry_manifest).unwrap();
383
384 let normalized_manifest_entry = entry_name.replace("%5F", "_");
390 Ok(Vc::upcast(VirtualOutputAsset::new_with_references(
391 node_root.join(&format!(
392 "server/app{normalized_manifest_entry}_client-reference-manifest.js",
393 ))?,
394 AssetContent::file(
395 File::from(formatdoc! {
396 r#"
397 globalThis.__RSC_MANIFEST = globalThis.__RSC_MANIFEST || {{}};
398 globalThis.__RSC_MANIFEST[{entry_name}] = {manifest}
399 "#,
400 entry_name = StringifyJs(&normalized_manifest_entry),
401 manifest = &client_reference_manifest_json
402 })
403 .into(),
404 ),
405 Vc::cell(references.into_iter().collect()),
406 )))
407 }
408 .instrument(span)
409 .await
410 }
411}
412
413impl From<&TurbopackModuleId> for ModuleId {
414 fn from(module_id: &TurbopackModuleId) -> Self {
415 match module_id {
416 TurbopackModuleId::String(string) => ModuleId::String(string.clone()),
417 TurbopackModuleId::Number(number) => ModuleId::Number(*number as _),
418 }
419 }
420}
421
422pub fn get_client_reference_module_key(server_path: &str, export_name: &str) -> RcStr {
424 if export_name == "*" {
425 server_path.into()
426 } else {
427 format!("{server_path}#{export_name}").into()
428 }
429}