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