Skip to main content

next_core/next_manifests/
mod.rs

1//! Type definitions for the Next.js manifest formats.
2
3pub mod client_reference_manifest;
4mod encode_uri_component;
5
6use anyhow::{Context, Result};
7use bincode::{Decode, Encode};
8use serde::{Deserialize, Serialize};
9use serde_json::value::RawValue;
10use turbo_rcstr::RcStr;
11use turbo_tasks::{FxIndexMap, ReadRef, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc};
12use turbo_tasks_fs::{File, FileContent, FileSystemPath};
13use turbopack_core::{
14    asset::{Asset, AssetContent},
15    output::{OutputAsset, OutputAssets, OutputAssetsReference, OutputAssetsWithReferenced},
16};
17
18use crate::next_config::RouteHas;
19
20#[derive(Serialize, Default, Debug)]
21pub struct PagesManifest {
22    #[serde(flatten)]
23    pub pages: FxIndexMap<RcStr, RcStr>,
24}
25
26#[derive(Debug)]
27#[turbo_tasks::value(shared)]
28pub struct BuildManifest {
29    pub output_path: FileSystemPath,
30    pub client_relative_path: FileSystemPath,
31
32    pub polyfill_files: Vec<ResolvedVc<Box<dyn OutputAsset>>>,
33    pub root_main_files: Vec<ResolvedVc<Box<dyn OutputAsset>>>,
34    #[bincode(with = "turbo_bincode::indexmap")]
35    pub pages: FxIndexMap<RcStr, ResolvedVc<OutputAssets>>,
36    /// Per-page extra files that supplement `root_main_files` for App Router
37    /// pages. Serialized as `rootMainFilesTree[page] = [...root_main_files,
38    /// ...per_page_files]` so that `required-scripts.tsx` can load the correct
39    /// page-specific scripts without polluting the shared `rootMainFiles`.
40    #[bincode(with = "turbo_bincode::indexmap")]
41    pub root_main_files_per_page: FxIndexMap<RcStr, Vec<ResolvedVc<Box<dyn OutputAsset>>>>,
42    /// Per-page inline chunk group bootstrap params, as JSON. Empty when the
43    /// bootstrap is emitted as a per-route chunk instead (e.g. dev).
44    #[bincode(with = "turbo_bincode::indexmap")]
45    pub pages_chunk_group_bootstrap_params: FxIndexMap<RcStr, RcStr>,
46    /// The `globalThis[...]` chunk-loading global the runtime drains.
47    pub chunk_loading_global: RcStr,
48}
49
50#[turbo_tasks::value_impl]
51impl OutputAssetsReference for BuildManifest {
52    #[turbo_tasks::function]
53    async fn references(&self) -> Result<Vc<OutputAssetsWithReferenced>> {
54        let chunks: Vec<ReadRef<OutputAssets>> = self.pages.values().try_join().await?;
55
56        let root_main_files = self
57            .root_main_files
58            .iter()
59            .map(async |c| Ok(c.path().await?.has_extension(".js").then_some(*c)))
60            .try_flat_join()
61            .await?;
62
63        let per_page_files = self
64            .root_main_files_per_page
65            .values()
66            .flatten()
67            .copied()
68            .collect::<Vec<_>>();
69
70        let references = chunks
71            .into_iter()
72            .flatten()
73            .chain(root_main_files)
74            .chain(self.polyfill_files.iter().copied())
75            .chain(per_page_files)
76            .collect();
77
78        Ok(OutputAssetsWithReferenced::from_assets(Vc::cell(
79            references,
80        )))
81    }
82}
83
84#[turbo_tasks::value_impl]
85impl OutputAsset for BuildManifest {
86    #[turbo_tasks::function]
87    async fn path(&self) -> Vc<FileSystemPath> {
88        self.output_path.clone().cell()
89    }
90}
91
92#[turbo_tasks::value_impl]
93impl Asset for BuildManifest {
94    #[turbo_tasks::function]
95    async fn content(&self) -> Result<Vc<AssetContent>> {
96        let client_relative_path = &self.client_relative_path;
97
98        #[derive(Serialize, Default, Debug)]
99        #[serde(rename_all = "camelCase")]
100        pub struct SerializedBuildManifest {
101            pub dev_files: Vec<RcStr>,
102            pub amp_dev_files: Vec<RcStr>,
103            pub polyfill_files: Vec<RcStr>,
104            pub low_priority_files: Vec<RcStr>,
105            pub root_main_files: Vec<RcStr>,
106            pub pages: FxIndexMap<RcStr, Vec<RcStr>>,
107            pub amp_first_pages: Vec<RcStr>,
108            pub root_main_files_tree: FxIndexMap<RcStr, Vec<RcStr>>,
109            // The values are already JSON; store them as `RawValue` so they are emitted verbatim.
110            pub pages_chunk_group_bootstrap_params: FxIndexMap<RcStr, Box<RawValue>>,
111            pub chunk_loading_global: RcStr,
112        }
113
114        let pages: Vec<(RcStr, Vec<RcStr>)> = self
115            .pages
116            .iter()
117            .map(async |(k, chunks)| {
118                Ok((
119                    k.clone(),
120                    chunks
121                        .await?
122                        .iter()
123                        .copied()
124                        .map(async |chunk| {
125                            let chunk_path = chunk.path().await?;
126                            Ok(client_relative_path
127                                .get_path_to(&chunk_path)
128                                .context("client chunk entry path must be inside the client root")?
129                                .into())
130                        })
131                        .try_join()
132                        .await?,
133                ))
134            })
135            .try_join()
136            .await?;
137
138        let polyfill_files: Vec<RcStr> = self
139            .polyfill_files
140            .iter()
141            .copied()
142            .map(async |chunk| {
143                let chunk_path = chunk.path().await?;
144                Ok(client_relative_path
145                    .get_path_to(&chunk_path)
146                    .context("failed to resolve client-relative path to polyfill")?
147                    .into())
148            })
149            .try_join()
150            .await?;
151
152        let root_main_files: Vec<RcStr> = self
153            .root_main_files
154            .iter()
155            .map(async |chunk| {
156                let chunk_path = chunk.path().await?;
157                if !chunk_path.has_extension(".js") {
158                    Ok(None)
159                } else {
160                    Ok(Some(
161                        client_relative_path
162                            .get_path_to(&chunk_path)
163                            .context("failed to resolve client-relative path to root_main_file")?
164                            .into(),
165                    ))
166                }
167            })
168            .try_flat_join()
169            .await?;
170
171        let root_main_files_tree: Vec<(RcStr, Vec<RcStr>)> = self
172            .root_main_files_per_page
173            .iter()
174            .map(async |(page, per_page_chunks)| {
175                let per_page_paths: Vec<RcStr> = per_page_chunks
176                    .iter()
177                    .copied()
178                    .map(async |chunk| {
179                        let chunk_path = chunk.path().await?;
180                        Ok(client_relative_path
181                            .get_path_to(&chunk_path)
182                            .context(
183                                "failed to resolve client-relative path to per-page root file",
184                            )?
185                            .into())
186                    })
187                    .try_join()
188                    .await?;
189                // Combine the shared root_main_files with this page's extra
190                // files so that required-scripts.tsx gets the full list.
191                let combined = root_main_files
192                    .iter()
193                    .cloned()
194                    .chain(per_page_paths)
195                    .collect();
196                Ok((page.clone(), combined))
197            })
198            .try_join()
199            .await?;
200
201        let manifest = SerializedBuildManifest {
202            pages: FxIndexMap::from_iter(pages),
203            polyfill_files,
204            root_main_files,
205            root_main_files_tree: FxIndexMap::from_iter(root_main_files_tree),
206            pages_chunk_group_bootstrap_params: self
207                .pages_chunk_group_bootstrap_params
208                .iter()
209                .map(|(k, v)| Ok((k.clone(), RawValue::from_string(v.to_string())?)))
210                .collect::<Result<FxIndexMap<_, _>>>()?,
211            chunk_loading_global: self.chunk_loading_global.clone(),
212            ..Default::default()
213        };
214
215        Ok(AssetContent::file(
216            FileContent::Content(File::from(serde_json::to_string_pretty(&manifest)?)).cell(),
217        ))
218    }
219}
220
221#[derive(Debug)]
222#[turbo_tasks::value(shared)]
223pub struct ClientBuildManifest {
224    pub output_path: FileSystemPath,
225    pub client_relative_path: FileSystemPath,
226
227    #[bincode(with = "turbo_bincode::indexmap")]
228    pub pages: FxIndexMap<RcStr, ResolvedVc<Box<dyn OutputAsset>>>,
229}
230
231#[turbo_tasks::value_impl]
232impl OutputAssetsReference for ClientBuildManifest {
233    #[turbo_tasks::function]
234    async fn references(&self) -> Result<Vc<OutputAssetsWithReferenced>> {
235        let chunks: Vec<ResolvedVc<Box<dyn OutputAsset>>> = self.pages.values().copied().collect();
236        Ok(OutputAssetsWithReferenced::from_assets(Vc::cell(chunks)))
237    }
238}
239
240#[turbo_tasks::value_impl]
241impl OutputAsset for ClientBuildManifest {
242    #[turbo_tasks::function]
243    async fn path(&self) -> Vc<FileSystemPath> {
244        self.output_path.clone().cell()
245    }
246}
247
248#[turbo_tasks::value_impl]
249impl Asset for ClientBuildManifest {
250    #[turbo_tasks::function]
251    async fn content(&self) -> Result<Vc<AssetContent>> {
252        let client_relative_path = &self.client_relative_path;
253
254        let manifest: FxIndexMap<RcStr, Vec<RcStr>> = self
255            .pages
256            .iter()
257            .map(async |(k, chunk)| {
258                Ok((
259                    k.clone(),
260                    vec![
261                        client_relative_path
262                            .get_path_to(&*chunk.path().await?)
263                            .context("client chunk entry path must be inside the client root")?
264                            .into(),
265                    ],
266                ))
267            })
268            .try_join()
269            .await?
270            .into_iter()
271            .collect();
272
273        Ok(AssetContent::file(
274            FileContent::Content(File::from(serde_json::to_string_pretty(&manifest)?)).cell(),
275        ))
276    }
277}
278
279#[derive(Serialize, Debug)]
280#[serde(rename_all = "camelCase", tag = "version")]
281#[allow(clippy::large_enum_variant)]
282pub enum MiddlewaresManifest {
283    #[serde(rename = "2")]
284    MiddlewaresManifestV2(MiddlewaresManifestV2),
285    #[serde(other)]
286    Unsupported,
287}
288
289impl Default for MiddlewaresManifest {
290    fn default() -> Self {
291        Self::MiddlewaresManifestV2(Default::default())
292    }
293}
294
295#[turbo_tasks::task_input]
296#[derive(
297    Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Encode, Decode,
298)]
299#[serde(rename_all = "camelCase", default)]
300pub struct ProxyMatcher {
301    // When skipped, next.js will fill the field during merging.
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub regexp: Option<RcStr>,
304    #[serde(skip_serializing_if = "bool_is_true")]
305    pub locale: bool,
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub has: Option<Vec<RouteHas>>,
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub missing: Option<Vec<RouteHas>>,
310    pub original_source: RcStr,
311}
312
313impl Default for ProxyMatcher {
314    fn default() -> Self {
315        Self {
316            regexp: None,
317            locale: true,
318            has: None,
319            missing: None,
320            original_source: Default::default(),
321        }
322    }
323}
324
325fn bool_is_true(b: &bool) -> bool {
326    *b
327}
328
329#[derive(Serialize, Default, Debug)]
330pub struct EdgeFunctionDefinition {
331    pub files: Vec<RcStr>,
332    pub name: RcStr,
333    pub page: RcStr,
334    pub entrypoint: RcStr,
335    pub matchers: Vec<ProxyMatcher>,
336    pub wasm: Vec<AssetBinding>,
337    pub assets: Vec<AssetBinding>,
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub regions: Option<Regions>,
340    pub env: FxIndexMap<RcStr, RcStr>,
341}
342
343#[derive(Serialize, Default, Debug)]
344pub struct InstrumentationDefinition {
345    pub files: Vec<RcStr>,
346    pub name: RcStr,
347    #[serde(skip_serializing_if = "Vec::is_empty")]
348    pub wasm: Vec<AssetBinding>,
349    #[serde(skip_serializing_if = "Vec::is_empty")]
350    pub assets: Vec<AssetBinding>,
351}
352
353#[derive(Serialize, Default, Debug)]
354#[serde(rename_all = "camelCase")]
355pub struct AssetBinding {
356    pub name: RcStr,
357    pub file_path: RcStr,
358}
359
360#[derive(Serialize, Debug)]
361#[serde(untagged)]
362pub enum Regions {
363    Multiple(Vec<RcStr>),
364    Single(RcStr),
365}
366
367#[derive(Serialize, Default, Debug)]
368pub struct MiddlewaresManifestV2 {
369    pub sorted_middleware: Vec<RcStr>,
370    pub middleware: FxIndexMap<RcStr, EdgeFunctionDefinition>,
371    pub instrumentation: Option<InstrumentationDefinition>,
372    pub functions: FxIndexMap<RcStr, EdgeFunctionDefinition>,
373}
374
375#[derive(Serialize, Default, Debug)]
376#[serde(rename_all = "camelCase")]
377pub struct ReactLoadableManifest {
378    #[serde(flatten)]
379    pub manifest: FxIndexMap<RcStr, ReactLoadableManifestEntry>,
380}
381
382#[derive(Serialize, Default, Debug)]
383#[serde(rename_all = "camelCase")]
384pub struct ReactLoadableManifestEntry {
385    pub id: u32,
386    pub files: Vec<RcStr>,
387}
388
389#[derive(Serialize, Default, Debug)]
390#[serde(rename_all = "camelCase")]
391pub struct NextFontManifest {
392    pub pages: FxIndexMap<RcStr, Vec<RcStr>>,
393    pub app: FxIndexMap<RcStr, Vec<RcStr>>,
394    pub app_using_size_adjust: bool,
395    pub pages_using_size_adjust: bool,
396}
397
398#[derive(Serialize, Default, Debug)]
399#[serde(rename_all = "camelCase")]
400pub struct AppPathsManifest {
401    #[serde(flatten)]
402    pub edge_server_app_paths: PagesManifest,
403    #[serde(flatten)]
404    pub node_server_app_paths: PagesManifest,
405}
406
407// A struct represent a single entry in react-loadable-manifest.json.
408// The manifest is in a format of:
409// { [`${origin} -> ${imported}`]: { id: `${origin} -> ${imported}`, files:
410// string[] } }
411#[derive(Serialize, Debug)]
412#[serde(rename_all = "camelCase")]
413pub struct LoadableManifest {
414    pub id: ModuleId,
415    pub files: Vec<RcStr>,
416}
417
418#[derive(Serialize, Default, Debug)]
419#[serde(rename_all = "camelCase")]
420pub struct ServerReferenceManifest<'a> {
421    /// A map from hashed action name to the runtime module we that exports it.
422    pub node: FxIndexMap<&'a str, ActionManifestEntry<'a>>,
423    /// A map from hashed action name to the runtime module we that exports it.
424    pub edge: FxIndexMap<&'a str, ActionManifestEntry<'a>>,
425}
426
427#[derive(Serialize, Default, Debug)]
428#[serde(rename_all = "camelCase")]
429pub struct ActionManifestEntry<'a> {
430    /// A mapping from the page that uses the server action to the runtime
431    /// module that exports it.
432    pub workers: FxIndexMap<&'a str, ActionManifestWorkerEntry<'a>>,
433
434    #[serde(rename = "exportedName")]
435    pub exported_name: &'a str,
436
437    pub filename: &'a str,
438
439    /// Source location line number (1-indexed), if available
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub line: Option<u32>,
442
443    /// Source location column number (1-indexed), if available
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub col: Option<u32>,
446}
447
448#[derive(Serialize, Debug)]
449#[serde(rename_all = "camelCase")]
450pub struct ActionManifestWorkerEntry<'a> {
451    pub module_id: ActionManifestModuleId<'a>,
452    #[serde(rename = "async")]
453    pub is_async: bool,
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub durability: Option<ActionManifestWorkerEntryDurability<'a>>,
456}
457
458#[derive(Serialize, Debug)]
459#[serde(rename_all = "camelCase")]
460pub struct ActionManifestWorkerEntryDurability<'a> {
461    pub code_hash: &'a str,
462    pub runtime_env_vars_read: &'a [RcStr],
463    pub runtime_env_vars_existence: &'a [RcStr],
464}
465
466#[derive(Serialize, Debug, Clone)]
467#[serde(untagged)]
468pub enum ActionManifestModuleId<'a> {
469    String(&'a str),
470    Number(u64),
471}
472
473#[turbo_tasks::task_input]
474#[derive(
475    Debug, Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Encode, Decode,
476)]
477#[serde(rename_all = "kebab-case")]
478pub enum ActionLayer {
479    Rsc,
480    ActionBrowser,
481}
482
483#[derive(Serialize, Debug, Eq, PartialEq, Hash, Clone)]
484#[serde(rename_all = "camelCase")]
485#[serde(untagged)]
486pub enum ModuleId {
487    String(RcStr),
488    Number(u64),
489}
490
491#[derive(Serialize, Default, Debug)]
492#[serde(rename_all = "camelCase")]
493pub struct FontManifest(pub Vec<FontManifestEntry>);
494
495#[derive(Serialize, Default, Debug)]
496#[serde(rename_all = "camelCase")]
497pub struct FontManifestEntry {
498    pub url: RcStr,
499    pub content: RcStr,
500}
501
502#[cfg(test)]
503mod tests {
504    use turbo_rcstr::rcstr;
505
506    use super::*;
507
508    #[test]
509    fn test_middleware_matcher_serialization() {
510        let matchers = vec![
511            ProxyMatcher {
512                regexp: None,
513                locale: false,
514                has: None,
515                missing: None,
516                original_source: rcstr!(""),
517            },
518            ProxyMatcher {
519                regexp: Some(rcstr!(".*")),
520                locale: true,
521                has: Some(vec![RouteHas::Query {
522                    key: rcstr!("foo"),
523                    value: None,
524                }]),
525                missing: Some(vec![RouteHas::Query {
526                    key: rcstr!("bar"),
527                    value: Some(rcstr!("value")),
528                }]),
529                original_source: rcstr!("source"),
530            },
531        ];
532
533        let serialized = serde_json::to_string(&matchers).unwrap();
534        let deserialized: Vec<ProxyMatcher> = serde_json::from_str(&serialized).unwrap();
535
536        assert_eq!(matchers, deserialized);
537    }
538}