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