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, 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().owned().await?;
75 let prefix_path = next_config
76 .computed_asset_prefix()
77 .owned()
78 .await?
79 .unwrap_or_default();
80 let suffix_path = chunk_suffix_path.unwrap_or_default();
81
82 entry_manifest.module_loading.cross_origin = next_config
86 .await?
87 .cross_origin
88 .as_ref()
89 .map(|p| p.to_owned());
90 let ClientReferencesChunks {
91 client_component_client_chunks,
92 layout_segment_client_chunks,
93 client_component_ssr_chunks,
94 } = &*client_references_chunks.await?;
95 let client_relative_path = client_relative_path.clone();
96 let node_root_ref = node_root.clone();
97
98 let client_references_ecmascript = client_references
99 .await?
100 .client_references
101 .iter()
102 .map(async |r| {
103 Ok(match r.ty() {
104 ClientReferenceType::EcmascriptClientReference(r) => Some((r, r.await?)),
105 ClientReferenceType::CssClientReference(_) => None,
106 })
107 })
108 .try_flat_join()
109 .await?;
110
111 let async_modules = async_module_info
112 .is_async_multiple(Vc::cell(
113 client_references_ecmascript
114 .iter()
115 .flat_map(|(r, r_val)| {
116 [
117 ResolvedVc::upcast(*r),
118 ResolvedVc::upcast(r_val.client_module),
119 ResolvedVc::upcast(r_val.ssr_module),
120 ]
121 })
122 .collect(),
123 ))
124 .await?;
125
126 async fn cached_chunk_paths(
127 cache: &mut FxHashMap<ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath>,
128 chunks: impl Iterator<Item = ResolvedVc<Box<dyn OutputAsset>>>,
129 ) -> Result<impl Iterator<Item = (ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath)>>
130 {
131 let results = chunks
132 .into_iter()
133 .map(|chunk| (chunk, cache.get(&chunk).cloned()))
134 .map(async |(chunk, path)| {
135 Ok(if let Some(path) = path {
136 (chunk, Either::Left(path))
137 } else {
138 (chunk, Either::Right(chunk.path().owned().await?))
139 })
140 })
141 .try_join()
142 .await?;
143
144 for (chunk, path) in &results {
145 if let Either::Right(path) = path {
146 cache.insert(*chunk, path.clone());
147 }
148 }
149 Ok(results.into_iter().map(|(chunk, path)| match path {
150 Either::Left(path) => (chunk, path),
151 Either::Right(path) => (chunk, path),
152 }))
153 }
154 let mut client_chunk_path_cache: FxHashMap<
155 ResolvedVc<Box<dyn OutputAsset>>,
156 FileSystemPath,
157 > = FxHashMap::default();
158 let mut ssr_chunk_path_cache: FxHashMap<
159 ResolvedVc<Box<dyn OutputAsset>>,
160 FileSystemPath,
161 > = FxHashMap::default();
162
163 for (client_reference_module, client_reference_module_ref) in
164 client_references_ecmascript
165 {
166 let app_client_reference_ty =
167 ClientReferenceType::EcmascriptClientReference(client_reference_module);
168
169 let server_path = client_reference_module_ref.server_ident.to_string().await?;
170 let client_module = client_reference_module_ref.client_module;
171 let client_chunk_item_id = client_module
172 .chunk_item_id(*ResolvedVc::upcast(client_chunking_context))
173 .await?;
174
175 let (client_chunks_paths, client_is_async) =
176 if let Some((client_chunks, _client_availability_info)) =
177 client_component_client_chunks.get(&app_client_reference_ty)
178 {
179 let client_chunks = client_chunks.await?;
180 references.extend(client_chunks.iter());
181 let client_chunks_paths = cached_chunk_paths(
182 &mut client_chunk_path_cache,
183 client_chunks.iter().copied(),
184 )
185 .await?;
186
187 let chunk_paths = client_chunks_paths
188 .filter_map(|(_, chunk_path)| {
189 client_relative_path
190 .get_path_to(&chunk_path)
191 .map(ToString::to_string)
192 })
193 .filter(|path| path.ends_with(".js"))
196 .map(|path| {
197 format!(
198 "{}{}{}",
199 prefix_path,
200 path.split('/').map(encode_uri_component).format("/"),
201 suffix_path
202 )
203 })
204 .map(RcStr::from)
205 .collect::<Vec<_>>();
206
207 let is_async = async_modules.contains(&ResolvedVc::upcast(client_module));
208
209 (chunk_paths, is_async)
210 } else {
211 (Vec::new(), false)
212 };
213
214 if let Some(ssr_chunking_context) = ssr_chunking_context {
215 let ssr_module = client_reference_module_ref.ssr_module;
216 let ssr_chunk_item_id = ssr_module
217 .chunk_item_id(*ResolvedVc::upcast(ssr_chunking_context))
218 .await?;
219
220 let rsc_chunk_item_id = client_reference_module
221 .chunk_item_id(*ResolvedVc::upcast(ssr_chunking_context))
222 .await?;
223
224 let (ssr_chunks_paths, ssr_is_async) = if runtime == NextRuntime::Edge {
225 (Vec::new(), false)
230 } else if let Some((ssr_chunks, _ssr_availability_info)) =
231 client_component_ssr_chunks.get(&app_client_reference_ty)
232 {
233 let ssr_chunks = ssr_chunks.await?;
234 references.extend(ssr_chunks.iter());
235
236 let ssr_chunks_paths = cached_chunk_paths(
237 &mut ssr_chunk_path_cache,
238 ssr_chunks.iter().copied(),
239 )
240 .await?;
241 let chunk_paths = ssr_chunks_paths
242 .filter_map(|(_, chunk_path)| {
243 node_root_ref
244 .get_path_to(&chunk_path)
245 .map(ToString::to_string)
246 })
247 .map(RcStr::from)
248 .collect::<Vec<_>>();
249
250 let is_async = async_modules.contains(&ResolvedVc::upcast(ssr_module));
251
252 (chunk_paths, is_async)
253 } else {
254 (Vec::new(), false)
255 };
256
257 let rsc_is_async = if runtime == NextRuntime::Edge {
258 false
259 } else {
260 async_modules.contains(&ResolvedVc::upcast(client_reference_module))
261 };
262
263 entry_manifest.client_modules.module_exports.insert(
264 get_client_reference_module_key(&server_path, "*"),
265 ManifestNodeEntry {
266 name: rcstr!("*"),
267 id: (&*client_chunk_item_id).into(),
268 chunks: client_chunks_paths,
269 r#async: client_is_async || ssr_is_async,
274 },
275 );
276
277 let mut ssr_manifest_node = ManifestNode::default();
278 ssr_manifest_node.module_exports.insert(
279 rcstr!("*"),
280 ManifestNodeEntry {
281 name: rcstr!("*"),
282 id: (&*ssr_chunk_item_id).into(),
283 chunks: ssr_chunks_paths,
284 r#async: client_is_async || ssr_is_async,
286 },
287 );
288
289 let mut rsc_manifest_node = ManifestNode::default();
290 rsc_manifest_node.module_exports.insert(
291 rcstr!("*"),
292 ManifestNodeEntry {
293 name: rcstr!("*"),
294 id: (&*rsc_chunk_item_id).into(),
295 chunks: vec![],
296 r#async: rsc_is_async,
297 },
298 );
299
300 match runtime {
301 NextRuntime::NodeJs => {
302 entry_manifest
303 .ssr_module_mapping
304 .insert((&*client_chunk_item_id).into(), ssr_manifest_node);
305 entry_manifest
306 .rsc_module_mapping
307 .insert((&*client_chunk_item_id).into(), rsc_manifest_node);
308 }
309 NextRuntime::Edge => {
310 entry_manifest
311 .edge_ssr_module_mapping
312 .insert((&*client_chunk_item_id).into(), ssr_manifest_node);
313 entry_manifest
314 .edge_rsc_module_mapping
315 .insert((&*client_chunk_item_id).into(), rsc_manifest_node);
316 }
317 }
318 }
319 }
320
321 for (server_component, client_chunks) in layout_segment_client_chunks.iter() {
323 let server_component_name = server_component
324 .server_path()
325 .await?
326 .with_extension("")
327 .value_to_string()
328 .owned()
329 .await?;
330 let entry_js_files = entry_manifest
331 .entry_js_files
332 .entry(server_component_name.clone())
333 .or_default();
334 let entry_css_files = entry_manifest
335 .entry_css_files
336 .entry(server_component_name)
337 .or_default();
338
339 let client_chunks = &client_chunks.await?;
340 let client_chunks_with_path =
341 cached_chunk_paths(&mut client_chunk_path_cache, client_chunks.iter().copied())
342 .await?;
343 let inlined_css = next_config.await?.experimental.inline_css.unwrap_or(false)
345 && mode.is_production();
346
347 for (chunk, chunk_path) in client_chunks_with_path {
348 if let Some(path) = client_relative_path.get_path_to(&chunk_path) {
349 let path = path.into();
352 if chunk_path.has_extension(".css") {
353 let content = if inlined_css {
354 Some(
355 if let Some(content_file) =
356 chunk.content().file_content().await?.as_content()
357 {
358 content_file.content().to_str()?.into()
359 } else {
360 RcStr::default()
361 },
362 )
363 } else {
364 None
365 };
366 entry_css_files.insert(CssResource {
367 path,
368 inlined: inlined_css,
369 content,
370 });
371 } else {
372 entry_js_files.insert(path);
373 }
374 }
375 }
376 }
377
378 let client_reference_manifest_json = serde_json::to_string(&entry_manifest).unwrap();
379
380 let normalized_manifest_entry = entry_name.replace("%5F", "_");
386 Ok(Vc::upcast(VirtualOutputAsset::new_with_references(
387 node_root.join(&format!(
388 "server/app{normalized_manifest_entry}_client-reference-manifest.js",
389 ))?,
390 AssetContent::file(
391 File::from(formatdoc! {
392 r#"
393 globalThis.__RSC_MANIFEST = globalThis.__RSC_MANIFEST || {{}};
394 globalThis.__RSC_MANIFEST[{entry_name}] = {manifest}
395 "#,
396 entry_name = StringifyJs(&normalized_manifest_entry),
397 manifest = &client_reference_manifest_json
398 })
399 .into(),
400 ),
401 Vc::cell(references.into_iter().collect()),
402 )))
403 }
404 .instrument(span)
405 .await
406 }
407}
408
409impl From<&TurbopackModuleId> for ModuleId {
410 fn from(module_id: &TurbopackModuleId) -> Self {
411 match module_id {
412 TurbopackModuleId::String(string) => ModuleId::String(string.clone()),
413 TurbopackModuleId::Number(number) => ModuleId::Number(*number as _),
414 }
415 }
416}
417
418pub fn get_client_reference_module_key(server_path: &str, export_name: &str) -> RcStr {
420 if export_name == "*" {
421 server_path.into()
422 } else {
423 format!("{server_path}#{export_name}").into()
424 }
425}