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