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::{Deserialize, Serialize};
7use tracing::Instrument;
8use turbo_rcstr::{RcStr, rcstr};
9use turbo_tasks::{
10    FxIndexSet, ResolvedVc, TaskInput, TryFlatJoinIterExt, TryJoinIterExt, ValueToString, Vc,
11    trace::TraceRawVcs,
12};
13use turbo_tasks_fs::{File, FileSystemPath};
14use turbopack_core::{
15    asset::{Asset, AssetContent},
16    chunk::{ChunkingContext, ModuleChunkItemIdExt, ModuleId as TurbopackModuleId},
17    module_graph::async_module_info::AsyncModulesInfo,
18    output::OutputAsset,
19    virtual_output::VirtualOutputAsset,
20};
21use turbopack_ecmascript::utils::StringifyJs;
22
23use super::{ClientReferenceManifest, CssResource, ManifestNode, ManifestNodeEntry, ModuleId};
24use crate::{
25    mode::NextMode,
26    next_app::ClientReferencesChunks,
27    next_client_reference::{ClientReferenceGraphResult, ClientReferenceType},
28    next_config::NextConfig,
29    next_manifests::encode_uri_component::encode_uri_component,
30    util::NextRuntime,
31};
32
33#[derive(TaskInput, Clone, Hash, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)]
34pub struct ClientReferenceManifestOptions {
35    pub node_root: FileSystemPath,
36    pub client_relative_path: FileSystemPath,
37    pub entry_name: RcStr,
38    pub client_references: ResolvedVc<ClientReferenceGraphResult>,
39    pub client_references_chunks: ResolvedVc<ClientReferencesChunks>,
40    pub client_chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
41    pub ssr_chunking_context: Option<ResolvedVc<Box<dyn ChunkingContext>>>,
42    pub async_module_info: ResolvedVc<AsyncModulesInfo>,
43    pub next_config: ResolvedVc<NextConfig>,
44    pub runtime: NextRuntime,
45    pub mode: NextMode,
46}
47
48#[turbo_tasks::value_impl]
49impl ClientReferenceManifest {
50    #[turbo_tasks::function]
51    pub async fn build_output(
52        options: ClientReferenceManifestOptions,
53    ) -> Result<Vc<Box<dyn OutputAsset>>> {
54        let ClientReferenceManifestOptions {
55            node_root,
56            client_relative_path,
57            entry_name,
58            client_references,
59            client_references_chunks,
60            client_chunking_context,
61            ssr_chunking_context,
62            async_module_info,
63            next_config,
64            runtime,
65            mode,
66        } = options;
67        let span = tracing::info_span!(
68            "ClientReferenceManifest build output",
69            entry_name = display(&entry_name)
70        );
71        async move {
72            let mut entry_manifest: ClientReferenceManifest = Default::default();
73            let mut references = FxIndexSet::default();
74            let chunk_suffix_path = next_config.chunk_suffix_path().owned().await?;
75            let prefix_path = next_config
76                .computed_asset_prefix()
77                .owned()
78                .await?
79                .unwrap_or_default();
80            let suffix_path = chunk_suffix_path.unwrap_or_default();
81
82            // TODO: Add `suffix` to the manifest for React to use.
83            // entry_manifest.module_loading.prefix = prefix_path;
84
85            entry_manifest.module_loading.cross_origin = next_config
86                .await?
87                .cross_origin
88                .as_ref()
89                .map(|p| p.to_owned());
90            let ClientReferencesChunks {
91                client_component_client_chunks,
92                layout_segment_client_chunks,
93                client_component_ssr_chunks,
94            } = &*client_references_chunks.await?;
95            let client_relative_path = client_relative_path.clone();
96            let node_root_ref = node_root.clone();
97
98            let client_references_ecmascript = client_references
99                .await?
100                .client_references
101                .iter()
102                .map(async |r| {
103                    Ok(match r.ty() {
104                        ClientReferenceType::EcmascriptClientReference(r) => Some((r, r.await?)),
105                        ClientReferenceType::CssClientReference(_) => None,
106                    })
107                })
108                .try_flat_join()
109                .await?;
110
111            let async_modules = async_module_info
112                .is_async_multiple(Vc::cell(
113                    client_references_ecmascript
114                        .iter()
115                        .flat_map(|(r, r_val)| {
116                            [
117                                ResolvedVc::upcast(*r),
118                                ResolvedVc::upcast(r_val.client_module),
119                                ResolvedVc::upcast(r_val.ssr_module),
120                            ]
121                        })
122                        .collect(),
123                ))
124                .await?;
125
126            async fn cached_chunk_paths(
127                cache: &mut FxHashMap<ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath>,
128                chunks: impl Iterator<Item = ResolvedVc<Box<dyn OutputAsset>>>,
129            ) -> Result<impl Iterator<Item = (ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath)>>
130            {
131                let results = chunks
132                    .into_iter()
133                    .map(|chunk| (chunk, cache.get(&chunk).cloned()))
134                    .map(async |(chunk, path)| {
135                        Ok(if let Some(path) = path {
136                            (chunk, Either::Left(path))
137                        } else {
138                            (chunk, Either::Right(chunk.path().owned().await?))
139                        })
140                    })
141                    .try_join()
142                    .await?;
143
144                for (chunk, path) in &results {
145                    if let Either::Right(path) = path {
146                        cache.insert(*chunk, path.clone());
147                    }
148                }
149                Ok(results.into_iter().map(|(chunk, path)| match path {
150                    Either::Left(path) => (chunk, path),
151                    Either::Right(path) => (chunk, path),
152                }))
153            }
154            let mut client_chunk_path_cache: FxHashMap<
155                ResolvedVc<Box<dyn OutputAsset>>,
156                FileSystemPath,
157            > = FxHashMap::default();
158            let mut ssr_chunk_path_cache: FxHashMap<
159                ResolvedVc<Box<dyn OutputAsset>>,
160                FileSystemPath,
161            > = FxHashMap::default();
162
163            for (client_reference_module, client_reference_module_ref) in
164                client_references_ecmascript
165            {
166                let app_client_reference_ty =
167                    ClientReferenceType::EcmascriptClientReference(client_reference_module);
168
169                let server_path = client_reference_module_ref.server_ident.to_string().await?;
170                let client_module = client_reference_module_ref.client_module;
171                let client_chunk_item_id = client_module
172                    .chunk_item_id(*ResolvedVc::upcast(client_chunking_context))
173                    .await?;
174
175                let (client_chunks_paths, client_is_async) =
176                    if let Some((client_chunks, _client_availability_info)) =
177                        client_component_client_chunks.get(&app_client_reference_ty)
178                    {
179                        let client_chunks = client_chunks.await?;
180                        references.extend(client_chunks.iter());
181                        let client_chunks_paths = cached_chunk_paths(
182                            &mut client_chunk_path_cache,
183                            client_chunks.iter().copied(),
184                        )
185                        .await?;
186
187                        let chunk_paths = client_chunks_paths
188                            .filter_map(|(_, chunk_path)| {
189                                client_relative_path
190                                    .get_path_to(&chunk_path)
191                                    .map(ToString::to_string)
192                            })
193                            // It's possible that a chunk also emits CSS files, that will
194                            // be handled separately.
195                            .filter(|path| path.ends_with(".js"))
196                            .map(|path| {
197                                format!(
198                                    "{}{}{}",
199                                    prefix_path,
200                                    path.split('/').map(encode_uri_component).format("/"),
201                                    suffix_path
202                                )
203                            })
204                            .map(RcStr::from)
205                            .collect::<Vec<_>>();
206
207                        let is_async = async_modules.contains(&ResolvedVc::upcast(client_module));
208
209                        (chunk_paths, is_async)
210                    } else {
211                        (Vec::new(), false)
212                    };
213
214                if let Some(ssr_chunking_context) = ssr_chunking_context {
215                    let ssr_module = client_reference_module_ref.ssr_module;
216                    let ssr_chunk_item_id = ssr_module
217                        .chunk_item_id(*ResolvedVc::upcast(ssr_chunking_context))
218                        .await?;
219
220                    let rsc_chunk_item_id = client_reference_module
221                        .chunk_item_id(*ResolvedVc::upcast(ssr_chunking_context))
222                        .await?;
223
224                    let (ssr_chunks_paths, ssr_is_async) = if runtime == NextRuntime::Edge {
225                        // the chunks get added to the middleware-manifest.json instead
226                        // of this file because the
227                        // edge runtime doesn't support dynamically
228                        // loading chunks.
229                        (Vec::new(), false)
230                    } else if let Some((ssr_chunks, _ssr_availability_info)) =
231                        client_component_ssr_chunks.get(&app_client_reference_ty)
232                    {
233                        let ssr_chunks = ssr_chunks.await?;
234                        references.extend(ssr_chunks.iter());
235
236                        let ssr_chunks_paths = cached_chunk_paths(
237                            &mut ssr_chunk_path_cache,
238                            ssr_chunks.iter().copied(),
239                        )
240                        .await?;
241                        let chunk_paths = ssr_chunks_paths
242                            .filter_map(|(_, chunk_path)| {
243                                node_root_ref
244                                    .get_path_to(&chunk_path)
245                                    .map(ToString::to_string)
246                            })
247                            .map(RcStr::from)
248                            .collect::<Vec<_>>();
249
250                        let is_async = async_modules.contains(&ResolvedVc::upcast(ssr_module));
251
252                        (chunk_paths, is_async)
253                    } else {
254                        (Vec::new(), false)
255                    };
256
257                    let rsc_is_async = if runtime == NextRuntime::Edge {
258                        false
259                    } else {
260                        async_modules.contains(&ResolvedVc::upcast(client_reference_module))
261                    };
262
263                    entry_manifest.client_modules.module_exports.insert(
264                        get_client_reference_module_key(&server_path, "*"),
265                        ManifestNodeEntry {
266                            name: rcstr!("*"),
267                            id: (&*client_chunk_item_id).into(),
268                            chunks: client_chunks_paths,
269                            // This should of course be client_is_async, but SSR can become
270                            // async due to ESM externals, and
271                            // the ssr_manifest_node is currently ignored
272                            // by React.
273                            r#async: client_is_async || ssr_is_async,
274                        },
275                    );
276
277                    let mut ssr_manifest_node = ManifestNode::default();
278                    ssr_manifest_node.module_exports.insert(
279                        rcstr!("*"),
280                        ManifestNodeEntry {
281                            name: rcstr!("*"),
282                            id: (&*ssr_chunk_item_id).into(),
283                            chunks: ssr_chunks_paths,
284                            // See above
285                            r#async: client_is_async || ssr_is_async,
286                        },
287                    );
288
289                    let mut rsc_manifest_node = ManifestNode::default();
290                    rsc_manifest_node.module_exports.insert(
291                        rcstr!("*"),
292                        ManifestNodeEntry {
293                            name: rcstr!("*"),
294                            id: (&*rsc_chunk_item_id).into(),
295                            chunks: vec![],
296                            r#async: rsc_is_async,
297                        },
298                    );
299
300                    match runtime {
301                        NextRuntime::NodeJs => {
302                            entry_manifest
303                                .ssr_module_mapping
304                                .insert((&*client_chunk_item_id).into(), ssr_manifest_node);
305                            entry_manifest
306                                .rsc_module_mapping
307                                .insert((&*client_chunk_item_id).into(), rsc_manifest_node);
308                        }
309                        NextRuntime::Edge => {
310                            entry_manifest
311                                .edge_ssr_module_mapping
312                                .insert((&*client_chunk_item_id).into(), ssr_manifest_node);
313                            entry_manifest
314                                .edge_rsc_module_mapping
315                                .insert((&*client_chunk_item_id).into(), rsc_manifest_node);
316                        }
317                    }
318                }
319            }
320
321            // per layout segment chunks need to be emitted into the manifest too
322            for (server_component, client_chunks) in layout_segment_client_chunks.iter() {
323                let server_component_name = server_component
324                    .server_path()
325                    .await?
326                    .with_extension("")
327                    .value_to_string()
328                    .owned()
329                    .await?;
330                let entry_js_files = entry_manifest
331                    .entry_js_files
332                    .entry(server_component_name.clone())
333                    .or_default();
334                let entry_css_files = entry_manifest
335                    .entry_css_files
336                    .entry(server_component_name)
337                    .or_default();
338
339                let client_chunks = &client_chunks.await?;
340                let client_chunks_with_path =
341                    cached_chunk_paths(&mut client_chunk_path_cache, client_chunks.iter().copied())
342                        .await?;
343                // Inlining breaks HMR so it is always disabled in dev.
344                let inlined_css = next_config.await?.experimental.inline_css.unwrap_or(false)
345                    && mode.is_production();
346
347                for (chunk, chunk_path) in client_chunks_with_path {
348                    if let Some(path) = client_relative_path.get_path_to(&chunk_path) {
349                        // The entry CSS files and entry JS files don't have prefix and suffix
350                        // applied because it is added by Next.js during rendering.
351                        let path = path.into();
352                        if chunk_path.has_extension(".css") {
353                            let content = if inlined_css {
354                                Some(
355                                    if let Some(content_file) =
356                                        chunk.content().file_content().await?.as_content()
357                                    {
358                                        content_file.content().to_str()?.into()
359                                    } else {
360                                        RcStr::default()
361                                    },
362                                )
363                            } else {
364                                None
365                            };
366                            entry_css_files.insert(CssResource {
367                                path,
368                                inlined: inlined_css,
369                                content,
370                            });
371                        } else {
372                            entry_js_files.insert(path);
373                        }
374                    }
375                }
376            }
377
378            let client_reference_manifest_json = serde_json::to_string(&entry_manifest).unwrap();
379
380            // We put normalized path for the each entry key and the manifest output path,
381            // to conform next.js's load client reference manifest behavior:
382            // https://github.com/vercel/next.js/blob/2f9d718695e4c90be13c3bf0f3647643533071bf/packages/next/src/server/load-components.ts#L162-L164
383            // note this only applies to the manifests, assets are placed to the original
384            // path still (same as webpack does)
385            let normalized_manifest_entry = entry_name.replace("%5F", "_");
386            Ok(Vc::upcast(VirtualOutputAsset::new_with_references(
387                node_root.join(&format!(
388                    "server/app{normalized_manifest_entry}_client-reference-manifest.js",
389                ))?,
390                AssetContent::file(
391                    File::from(formatdoc! {
392                        r#"
393                        globalThis.__RSC_MANIFEST = globalThis.__RSC_MANIFEST || {{}};
394                        globalThis.__RSC_MANIFEST[{entry_name}] = {manifest}
395                    "#,
396                        entry_name = StringifyJs(&normalized_manifest_entry),
397                        manifest = &client_reference_manifest_json
398                    })
399                    .into(),
400                ),
401                Vc::cell(references.into_iter().collect()),
402            )))
403        }
404        .instrument(span)
405        .await
406    }
407}
408
409impl From<&TurbopackModuleId> for ModuleId {
410    fn from(module_id: &TurbopackModuleId) -> Self {
411        match module_id {
412            TurbopackModuleId::String(string) => ModuleId::String(string.clone()),
413            TurbopackModuleId::Number(number) => ModuleId::Number(*number as _),
414        }
415    }
416}
417
418/// See next.js/packages/next/src/lib/client-reference.ts
419pub fn get_client_reference_module_key(server_path: &str, export_name: &str) -> RcStr {
420    if export_name == "*" {
421        server_path.into()
422    } else {
423        format!("{server_path}#{export_name}").into()
424    }
425}