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