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