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