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