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