Skip to main content

next_core/next_manifests/
client_reference_manifest.rs

1use anyhow::Result;
2use either::Either;
3use indoc::formatdoc;
4use itertools::Itertools;
5use rustc_hash::{FxHashMap, FxHashSet};
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::{
17        ChunkingContext, CrossOrigin, ModuleChunkItemIdExt, ModuleId as TurbopackModuleId,
18        OutputChunk,
19    },
20    module_graph::async_module_info::AsyncModulesInfo,
21    output::{OutputAsset, OutputAssets, OutputAssetsReference, OutputAssetsWithReferenced},
22};
23use turbopack_ecmascript::utils::StringifyJs;
24
25use crate::{
26    mode::NextMode,
27    next_app::ClientReferencesChunks,
28    next_client_reference::{ClientReferenceGraphResult, ClientReferenceType},
29    next_config::NextConfig,
30    next_manifests::{ModuleId, encode_uri_component::encode_uri_component},
31    util::NextRuntime,
32};
33
34#[derive(Serialize, Default, Debug)]
35#[serde(rename_all = "camelCase")]
36pub struct SerializedClientReferenceManifest {
37    pub module_loading: ModuleLoading,
38    /// Mapping of module path and export name to client module ID and required
39    /// client chunks.
40    pub client_modules: ManifestNode,
41    /// Mapping of client module ID to corresponding SSR module ID and required
42    /// SSR chunks.
43    pub ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
44    /// Same as `ssr_module_mapping`, but for Edge SSR.
45    #[serde(rename = "edgeSSRModuleMapping")]
46    pub edge_ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
47    /// Mapping of client module ID to corresponding RSC module ID and required
48    /// RSC chunks.
49    pub rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
50    /// Same as `rsc_module_mapping`, but for Edge RSC.
51    #[serde(rename = "edgeRscModuleMapping")]
52    pub edge_rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
53    /// Mapping of server component path to required CSS client chunks.
54    #[serde(rename = "entryCSSFiles")]
55    pub entry_css_files: FxIndexMap<RcStr, FxIndexSet<CssResource>>,
56    /// Mapping of server component path to required JS client chunks.
57    #[serde(rename = "entryJSFiles")]
58    pub entry_js_files: FxIndexMap<RcStr, FxIndexSet<RcStr>>,
59}
60
61#[derive(Serialize, Debug, Clone, Eq, Hash, PartialEq)]
62pub struct CssResource {
63    pub path: RcStr,
64    pub inlined: bool,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub content: Option<RcStr>,
67}
68
69#[derive(Serialize, Default, Debug)]
70#[serde(rename_all = "camelCase")]
71pub struct ModuleLoading {
72    pub prefix: RcStr,
73    pub cross_origin: CrossOrigin,
74}
75
76#[derive(Serialize, Default, Debug, Clone)]
77#[serde(rename_all = "camelCase")]
78pub struct ManifestNode {
79    /// Mapping of export name to manifest node entry.
80    #[serde(flatten)]
81    pub module_exports: FxIndexMap<RcStr, ManifestNodeEntry>,
82}
83
84#[derive(Serialize, Debug, Clone)]
85#[serde(rename_all = "camelCase")]
86pub struct ManifestNodeEntry {
87    /// Turbopack module ID.
88    pub id: ModuleId,
89    /// Export name.
90    pub name: RcStr,
91    /// Chunks for the module. JS and CSS.
92    pub chunks: Vec<ClientChunk>,
93    // TODO(WEB-434)
94    pub r#async: bool,
95}
96
97/// One entry in a `ManifestNodeEntry.chunks` array, as consumed by React's Flight client via
98/// `__turbopack_load_by_url__`.
99///
100/// Most chunks are a plain URL string. A *merged* chunk (one that bundles several component
101/// chunks) is instead emitted as a `[url, componentChunkPaths, componentChunkSizes]` array. This
102/// us to dynamically choose to load the whole chunk or individual components of it, as neeeded.
103/// The sizes (bytes of the emitted files) feed the runtime's split-vs-whole cost heuristic.
104#[derive(Serialize, Debug, Clone)]
105#[serde(untagged)]
106pub enum ClientChunk {
107    Path(RcStr),
108    Merged(RcStr, Vec<RcStr>, Vec<u64>),
109}
110
111#[turbo_tasks::value(shared)]
112pub struct ClientReferenceManifest {
113    pub node_root: FileSystemPath,
114    pub client_relative_path: FileSystemPath,
115    pub entry_name: RcStr,
116    pub client_references: ResolvedVc<ClientReferenceGraphResult>,
117    pub client_references_chunks: ResolvedVc<ClientReferencesChunks>,
118    pub client_chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
119    pub ssr_chunking_context: Option<ResolvedVc<Box<dyn ChunkingContext>>>,
120    pub async_module_info: ResolvedVc<AsyncModulesInfo>,
121    pub next_config: ResolvedVc<NextConfig>,
122    pub runtime: NextRuntime,
123    pub mode: NextMode,
124}
125
126#[turbo_tasks::value_impl]
127impl OutputAssetsReference for ClientReferenceManifest {
128    #[turbo_tasks::function]
129    async fn references(self: Vc<Self>) -> Result<Vc<OutputAssetsWithReferenced>> {
130        Ok(OutputAssetsWithReferenced::from_assets(
131            *build_manifest(self).await?.references,
132        ))
133    }
134}
135
136#[turbo_tasks::value_impl]
137impl OutputAsset for ClientReferenceManifest {
138    #[turbo_tasks::function]
139    async fn path(&self) -> Result<Vc<FileSystemPath>> {
140        let normalized_manifest_entry = self.entry_name.replace("%5F", "_");
141        Ok(self
142            .node_root
143            .join(&format!(
144                "server/app{normalized_manifest_entry}_client-reference-manifest.js",
145            ))?
146            .cell())
147    }
148}
149
150#[turbo_tasks::value_impl]
151impl Asset for ClientReferenceManifest {
152    #[turbo_tasks::function]
153    async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
154        Ok(*build_manifest(self).await?.content)
155    }
156}
157
158#[turbo_tasks::value(shared)]
159struct ClientReferenceManifestResult {
160    content: ResolvedVc<AssetContent>,
161    references: ResolvedVc<OutputAssets>,
162}
163
164#[turbo_tasks::function]
165async fn build_manifest(
166    manifest: Vc<ClientReferenceManifest>,
167) -> Result<Vc<ClientReferenceManifestResult>> {
168    let ClientReferenceManifest {
169        node_root,
170        client_relative_path,
171        entry_name,
172        client_references,
173        client_references_chunks,
174        client_chunking_context,
175        ssr_chunking_context,
176        async_module_info,
177        next_config,
178        runtime,
179        mode,
180    } = &*manifest.await?;
181    let span = tracing::info_span!(
182        "build client reference manifest",
183        entry_name = display(&entry_name)
184    );
185    async move {
186        let mut entry_manifest: SerializedClientReferenceManifest = Default::default();
187        let mut references = FxIndexSet::default();
188        let prefix_path = next_config.computed_asset_prefix().owned().await?;
189        let asset_suffix_path = next_config.asset_suffix_path().owned().await?;
190        let add_deployment_id_at_runtime = *next_config
191            .should_append_server_deployment_id_at_runtime()
192            .await?;
193        let suffix_path = if !add_deployment_id_at_runtime {
194            asset_suffix_path.unwrap_or_default()
195        } else {
196            rcstr!("")
197        };
198
199        entry_manifest.module_loading.cross_origin = *next_config.cross_origin().await?;
200        let ClientReferencesChunks {
201            client_component_client_chunks,
202            layout_segment_client_chunks,
203            client_component_ssr_chunks,
204        } = &*client_references_chunks.await?;
205        let client_relative_path = client_relative_path.clone();
206        let node_root_ref = node_root.clone();
207
208        let client_references_ecmascript = client_references
209            .await?
210            .client_references
211            .iter()
212            .map(async |r| {
213                Ok(match r.ty {
214                    ClientReferenceType::EcmascriptClientReference(r) => Some((r, r.await?)),
215                    ClientReferenceType::CssClientReference(_) => None,
216                })
217            })
218            .try_flat_join()
219            .await?;
220
221        let async_modules = client_references_ecmascript
222            .iter()
223            .flat_map(|(r, r_val)| {
224                [
225                    ResolvedVc::upcast(*r),
226                    ResolvedVc::upcast(r_val.client_module),
227                    ResolvedVc::upcast(r_val.ssr_module),
228                ]
229            })
230            .map(async move |asset| {
231                Ok(if async_module_info.is_async(asset).await? {
232                    Some(asset)
233                } else {
234                    None
235                })
236            })
237            .try_flat_join()
238            .await?;
239
240        async fn cached_chunk_paths(
241            cache: &mut FxHashMap<ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath>,
242            chunks: impl Iterator<Item = ResolvedVc<Box<dyn OutputAsset>>>,
243        ) -> Result<impl Iterator<Item = (ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath)>>
244        {
245            let results = chunks
246                .into_iter()
247                .map(|chunk| (chunk, cache.get(&chunk).cloned()))
248                .map(async |(chunk, path)| {
249                    Ok(if let Some(path) = path {
250                        (chunk, Either::Left(path))
251                    } else {
252                        (chunk, Either::Right(chunk.path().owned().await?))
253                    })
254                })
255                .try_join()
256                .await?;
257
258            for (chunk, path) in &results {
259                if let Either::Right(path) = path {
260                    cache.insert(*chunk, path.clone());
261                }
262            }
263            Ok(results.into_iter().map(|(chunk, path)| match path {
264                Either::Left(path) => (chunk, path),
265                Either::Right(path) => (chunk, path),
266            }))
267        }
268        let mut client_chunk_path_cache: FxHashMap<
269            ResolvedVc<Box<dyn OutputAsset>>,
270            FileSystemPath,
271        > = FxHashMap::default();
272        let mut ssr_chunk_path_cache: FxHashMap<ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath> =
273            FxHashMap::default();
274
275        let mut client_reference_chunk_paths: FxHashSet<RcStr> = FxHashSet::default();
276
277        for (client_reference_module, client_reference_module_ref) in client_references_ecmascript {
278            let app_client_reference_ty =
279                ClientReferenceType::EcmascriptClientReference(client_reference_module);
280
281            let server_path = client_reference_module_ref.server_ident.to_string().await?;
282            let client_module = client_reference_module_ref.client_module;
283            let client_chunk_item_id = client_module
284                .chunk_item_id(**client_chunking_context)
285                .await?;
286
287            let (client_chunks_paths, client_is_async) = if let Some(client_assets) =
288                client_component_client_chunks.get(&app_client_reference_ty)
289            {
290                let client_chunks = client_assets.primary_assets().await?;
291                let client_referenced_assets = client_assets.referenced_assets().await?;
292                references.extend(client_chunks.iter());
293                references.extend(client_referenced_assets.iter());
294
295                let client_chunks_paths =
296                    cached_chunk_paths(&mut client_chunk_path_cache, client_chunks.iter().copied())
297                        .await?;
298
299                let js_chunks = client_chunks_paths
300                    .filter_map(|(chunk, chunk_path)| {
301                        client_relative_path
302                            .get_path_to(&chunk_path)
303                            .map(|path| (chunk, path.to_string()))
304                    })
305                    // It's possible that a chunk also emits CSS files, that will
306                    // be handled separately.
307                    .filter(|(_, path)| path.ends_with(".js"))
308                    .collect::<Vec<_>>();
309
310                for (_, path) in &js_chunks {
311                    client_reference_chunk_paths.insert(RcStr::from(path.as_str()));
312                }
313
314                let chunk_paths = js_chunks
315                    .into_iter()
316                    .map(async |(chunk, path)| {
317                        let url = RcStr::from(format!(
318                            "{}{}{}",
319                            prefix_path,
320                            path.split('/').map(encode_uri_component).format("/"),
321                            suffix_path
322                        ));
323                        // If this is a merged chunk, emit its component chunk paths alongside the
324                        // URL so the browser runtime can split it during navigation.
325                        let components =
326                            client_chunk_components(chunk, &client_relative_path).await?;
327                        Ok(if components.is_empty() {
328                            ClientChunk::Path(url)
329                        } else {
330                            let (paths, sizes) = components.into_iter().unzip();
331                            ClientChunk::Merged(url, paths, sizes)
332                        })
333                    })
334                    .try_join()
335                    .await?;
336
337                let is_async = async_modules.contains(&ResolvedVc::upcast(client_module));
338
339                (chunk_paths, is_async)
340            } else {
341                (Vec::new(), false)
342            };
343
344            if let Some(ssr_chunking_context) = *ssr_chunking_context {
345                let ssr_module = client_reference_module_ref.ssr_module;
346                let ssr_chunk_item_id = ssr_module.chunk_item_id(*ssr_chunking_context).await?;
347
348                let rsc_chunk_item_id = client_reference_module
349                    .chunk_item_id(*ssr_chunking_context)
350                    .await?;
351
352                let (ssr_chunks_paths, ssr_is_async) = if *runtime == NextRuntime::Edge {
353                    // the chunks get added to the middleware-manifest.json instead
354                    // of this file because the
355                    // edge runtime doesn't support dynamically
356                    // loading chunks.
357                    (Vec::new(), false)
358                } else if let Some(ssr_assets) =
359                    client_component_ssr_chunks.get(&app_client_reference_ty)
360                {
361                    let ssr_chunks = ssr_assets.primary_assets().await?;
362                    let ssr_referenced_assets = ssr_assets.referenced_assets().await?;
363                    references.extend(ssr_chunks.iter());
364                    references.extend(ssr_referenced_assets.iter());
365
366                    let ssr_chunks_paths =
367                        cached_chunk_paths(&mut ssr_chunk_path_cache, ssr_chunks.iter().copied())
368                            .await?;
369                    let chunk_paths = ssr_chunks_paths
370                        .filter_map(|(_, chunk_path)| {
371                            node_root_ref
372                                .get_path_to(&chunk_path)
373                                .map(ToString::to_string)
374                        })
375                        .map(RcStr::from)
376                        .collect::<Vec<_>>();
377
378                    let is_async = async_modules.contains(&ResolvedVc::upcast(ssr_module));
379
380                    (chunk_paths, is_async)
381                } else {
382                    (Vec::new(), false)
383                };
384
385                let rsc_is_async = if *runtime == NextRuntime::Edge {
386                    false
387                } else {
388                    async_modules.contains(&ResolvedVc::upcast(client_reference_module))
389                };
390
391                entry_manifest.client_modules.module_exports.insert(
392                    get_client_reference_module_key(&server_path, "*"),
393                    ManifestNodeEntry {
394                        name: rcstr!("*"),
395                        id: (&client_chunk_item_id).into(),
396                        chunks: client_chunks_paths,
397                        // This should of course be client_is_async, but SSR can become
398                        // async due to ESM externals, and
399                        // the ssr_manifest_node is currently ignored
400                        // by React.
401                        r#async: client_is_async || ssr_is_async,
402                    },
403                );
404
405                let mut ssr_manifest_node = ManifestNode::default();
406                ssr_manifest_node.module_exports.insert(
407                    rcstr!("*"),
408                    ManifestNodeEntry {
409                        name: rcstr!("*"),
410                        id: (&ssr_chunk_item_id).into(),
411                        chunks: ssr_chunks_paths
412                            .into_iter()
413                            .map(ClientChunk::Path)
414                            .collect(),
415                        // See above
416                        r#async: client_is_async || ssr_is_async,
417                    },
418                );
419
420                let mut rsc_manifest_node = ManifestNode::default();
421                rsc_manifest_node.module_exports.insert(
422                    rcstr!("*"),
423                    ManifestNodeEntry {
424                        name: rcstr!("*"),
425                        id: (&rsc_chunk_item_id).into(),
426                        chunks: vec![],
427                        r#async: rsc_is_async,
428                    },
429                );
430
431                match runtime {
432                    NextRuntime::NodeJs => {
433                        entry_manifest
434                            .ssr_module_mapping
435                            .insert((&client_chunk_item_id).into(), ssr_manifest_node);
436                        entry_manifest
437                            .rsc_module_mapping
438                            .insert((&client_chunk_item_id).into(), rsc_manifest_node);
439                    }
440                    NextRuntime::Edge => {
441                        entry_manifest
442                            .edge_ssr_module_mapping
443                            .insert((&client_chunk_item_id).into(), ssr_manifest_node);
444                        entry_manifest
445                            .edge_rsc_module_mapping
446                            .insert((&client_chunk_item_id).into(), rsc_manifest_node);
447                    }
448                }
449            }
450        }
451
452        // per layout segment chunks need to be emitted into the manifest too
453        for (server_component, client_assets) in layout_segment_client_chunks.iter() {
454            // Use source_path() to get the original source path (e.g., page.mdx) instead of
455            // server_path() which returns the transformed path (e.g., page.mdx.tsx).
456            // This ensures the manifest key matches what the LoaderTree stores and what
457            // the runtime looks up after stripping one extension.
458            let server_component_name = server_component
459                .source_path()
460                .await?
461                .with_extension("")
462                .to_string_ref()
463                .await?;
464            let entry_js_files = entry_manifest
465                .entry_js_files
466                .entry(server_component_name.clone())
467                .or_default();
468            let entry_css_files = entry_manifest
469                .entry_css_files
470                .entry(server_component_name)
471                .or_default();
472
473            let client_chunks = client_assets.primary_assets().await?;
474            let client_chunks_with_path =
475                cached_chunk_paths(&mut client_chunk_path_cache, client_chunks.iter().copied())
476                    .await?;
477            // Inlining breaks HMR so it is always disabled in dev.
478            let inlined_css = *next_config.inline_css().await? && mode.is_production();
479            // Component chunks are also exposed via `clientModules[].chunks`, so when the feature
480            // is on we drop them from `entryJSFiles` to avoid double-listing. When it's off the
481            // manifest must match the non-component-chunk output exactly.
482            let generate_component_chunks =
483                *next_config.turbopack_generate_component_chunks().await?;
484
485            for (chunk, chunk_path) in client_chunks_with_path {
486                if let Some(path) = client_relative_path.get_path_to(&chunk_path) {
487                    // The entry CSS files and entry JS files don't have prefix and suffix
488                    // applied because it is added by Next.js during rendering.
489                    let path = path.into();
490                    if chunk_path.has_extension(".css") {
491                        let content = if inlined_css {
492                            Some(
493                                if let Some(content_file) =
494                                    chunk.content().file_content().await?.as_content()
495                                {
496                                    content_file.content().to_str()?.into()
497                                } else {
498                                    RcStr::default()
499                                },
500                            )
501                        } else {
502                            None
503                        };
504                        entry_css_files.insert(CssResource {
505                            path,
506                            inlined: inlined_css,
507                            content,
508                        });
509                    } else if !mode.is_production()
510                        || !generate_component_chunks
511                        || !client_reference_chunk_paths.contains(&path)
512                    {
513                        entry_js_files.insert(path);
514                    }
515                }
516            }
517        }
518
519        let client_reference_manifest_json = serde_json::to_string(&entry_manifest).unwrap();
520
521        // We put normalized path for the each entry key and the manifest output path,
522        // to conform next.js's load client reference manifest behavior:
523        // https://github.com/vercel/next.js/blob/2f9d718695e4c90be13c3bf0f3647643533071bf/packages/next/src/server/load-components.ts#L162-L164
524        // note this only applies to the manifests, assets are placed to the original
525        // path still (same as webpack does)
526        let normalized_manifest_entry = entry_name.replace("%5F", "_");
527        Ok(ClientReferenceManifestResult {
528            content: AssetContent::file(
529                FileContent::Content(File::from(formatdoc! {
530                    r#"
531                        globalThis.__RSC_MANIFEST = globalThis.__RSC_MANIFEST || {{}};
532                        globalThis.__RSC_MANIFEST[{entry_name}] = {manifest};
533                        {suffix}
534                    "#,
535                    entry_name = StringifyJs(&normalized_manifest_entry),
536                    manifest = &client_reference_manifest_json,
537                    suffix = if add_deployment_id_at_runtime {
538                        formatdoc!{
539                            r#"
540                            for (const key in globalThis.__RSC_MANIFEST[{entry_name}].clientModules) {{
541                                const val = {{ ...globalThis.__RSC_MANIFEST[{entry_name}].clientModules[key] }}
542                                globalThis.__RSC_MANIFEST[{entry_name}].clientModules[key] = val
543                                val.chunks = val.chunks.map((c) =>
544                                    typeof c === 'string'
545                                        ? `${{c}}?dpl=${{process.env.NEXT_DEPLOYMENT_ID}}`
546                                        : [`${{c[0]}}?dpl=${{process.env.NEXT_DEPLOYMENT_ID}}`, c[1], c[2]])
547                            }}
548                            "#,
549                            entry_name = StringifyJs(&normalized_manifest_entry),
550                        }
551                    } else {
552                        "".to_string()
553                    }
554                }))
555                .cell(),
556            )
557            .to_resolved()
558            .await?,
559            references: ResolvedVc::cell(references.into_iter().collect()),
560        }
561        .cell())
562    }
563    .instrument(span)
564    .await
565}
566
567impl From<&TurbopackModuleId> for ModuleId {
568    fn from(module_id: &TurbopackModuleId) -> Self {
569        match module_id {
570            TurbopackModuleId::String(string) => ModuleId::String(string.clone()),
571            TurbopackModuleId::Number(number) => ModuleId::Number(*number as _),
572        }
573    }
574}
575
576async fn client_chunk_components(
577    chunk: ResolvedVc<Box<dyn OutputAsset>>,
578    client_relative_path: &FileSystemPath,
579) -> Result<Vec<(RcStr, u64)>> {
580    let Some(output_chunk) = ResolvedVc::try_sidecast::<Box<dyn OutputChunk>>(chunk) else {
581        return Ok(Vec::new());
582    };
583    let Some(component_chunks) = output_chunk.runtime_info().await?.module_chunks else {
584        return Ok(Vec::new());
585    };
586    let component_assets = component_chunks.await?;
587    let mut components = Vec::with_capacity(component_assets.len());
588    for component in component_assets.iter() {
589        let component_path = component.path().await?;
590        if let Some(rel) = client_relative_path.get_path_to(&component_path)
591            && rel.ends_with(".js")
592        {
593            let size = component
594                .content()
595                .file_content()
596                .await?
597                .as_content()
598                .map_or(0, |file| file.content().len() as u64);
599            components.push((RcStr::from(rel), size));
600        }
601    }
602    Ok(components)
603}
604
605/// See next.js/packages/next/src/lib/client-reference.ts
606pub fn get_client_reference_module_key(server_path: &str, export_name: &str) -> RcStr {
607    if export_name == "*" {
608        server_path.into()
609    } else {
610        format!("{server_path}#{export_name}").into()
611    }
612}