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 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)
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),
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 entrypoint: RcStr,
289    pub matchers: Vec<ProxyMatcher>,
290    pub wasm: Vec<AssetBinding>,
291    pub assets: Vec<AssetBinding>,
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub regions: Option<Regions>,
294    pub env: FxIndexMap<RcStr, RcStr>,
295}
296
297#[derive(Serialize, Default, Debug)]
298pub struct InstrumentationDefinition {
299    pub files: Vec<RcStr>,
300    pub name: RcStr,
301    #[serde(skip_serializing_if = "Vec::is_empty")]
302    pub wasm: Vec<AssetBinding>,
303    #[serde(skip_serializing_if = "Vec::is_empty")]
304    pub assets: Vec<AssetBinding>,
305}
306
307#[derive(Serialize, Default, Debug)]
308#[serde(rename_all = "camelCase")]
309pub struct AssetBinding {
310    pub name: RcStr,
311    pub file_path: RcStr,
312}
313
314#[derive(Serialize, Debug)]
315#[serde(untagged)]
316pub enum Regions {
317    Multiple(Vec<RcStr>),
318    Single(RcStr),
319}
320
321#[derive(Serialize, Default, Debug)]
322pub struct MiddlewaresManifestV2 {
323    pub sorted_middleware: Vec<RcStr>,
324    pub middleware: FxIndexMap<RcStr, EdgeFunctionDefinition>,
325    pub instrumentation: Option<InstrumentationDefinition>,
326    pub functions: FxIndexMap<RcStr, EdgeFunctionDefinition>,
327}
328
329#[derive(Serialize, Default, Debug)]
330#[serde(rename_all = "camelCase")]
331pub struct ReactLoadableManifest {
332    #[serde(flatten)]
333    pub manifest: FxIndexMap<RcStr, ReactLoadableManifestEntry>,
334}
335
336#[derive(Serialize, Default, Debug)]
337#[serde(rename_all = "camelCase")]
338pub struct ReactLoadableManifestEntry {
339    pub id: u32,
340    pub files: Vec<RcStr>,
341}
342
343#[derive(Serialize, Default, Debug)]
344#[serde(rename_all = "camelCase")]
345pub struct NextFontManifest {
346    pub pages: FxIndexMap<RcStr, Vec<RcStr>>,
347    pub app: FxIndexMap<RcStr, Vec<RcStr>>,
348    pub app_using_size_adjust: bool,
349    pub pages_using_size_adjust: bool,
350}
351
352#[derive(Serialize, Default, Debug)]
353#[serde(rename_all = "camelCase")]
354pub struct AppPathsManifest {
355    #[serde(flatten)]
356    pub edge_server_app_paths: PagesManifest,
357    #[serde(flatten)]
358    pub node_server_app_paths: PagesManifest,
359}
360
361// A struct represent a single entry in react-loadable-manifest.json.
362// The manifest is in a format of:
363// { [`${origin} -> ${imported}`]: { id: `${origin} -> ${imported}`, files:
364// string[] } }
365#[derive(Serialize, Debug)]
366#[serde(rename_all = "camelCase")]
367pub struct LoadableManifest {
368    pub id: ModuleId,
369    pub files: Vec<RcStr>,
370}
371
372#[derive(Serialize, Default, Debug)]
373#[serde(rename_all = "camelCase")]
374pub struct ServerReferenceManifest<'a> {
375    /// A map from hashed action name to the runtime module we that exports it.
376    pub node: FxIndexMap<&'a str, ActionManifestEntry<'a>>,
377    /// A map from hashed action name to the runtime module we that exports it.
378    pub edge: FxIndexMap<&'a str, ActionManifestEntry<'a>>,
379}
380
381#[derive(Serialize, Default, Debug)]
382#[serde(rename_all = "camelCase")]
383pub struct ActionManifestEntry<'a> {
384    /// A mapping from the page that uses the server action to the runtime
385    /// module that exports it.
386    pub workers: FxIndexMap<&'a str, ActionManifestWorkerEntry<'a>>,
387
388    #[serde(rename = "exportedName")]
389    pub exported_name: &'a str,
390
391    pub filename: &'a str,
392
393    /// Source location line number (1-indexed), if available
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub line: Option<u32>,
396
397    /// Source location column number (1-indexed), if available
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub col: Option<u32>,
400}
401
402#[derive(Serialize, Debug)]
403pub struct ActionManifestWorkerEntry<'a> {
404    #[serde(rename = "moduleId")]
405    pub module_id: ActionManifestModuleId<'a>,
406    #[serde(rename = "async")]
407    pub is_async: bool,
408    #[serde(rename = "exportedName")]
409    pub exported_name: &'a str,
410    pub filename: &'a str,
411}
412
413#[derive(Serialize, Debug, Clone)]
414#[serde(untagged)]
415pub enum ActionManifestModuleId<'a> {
416    String(&'a str),
417    Number(u64),
418}
419
420#[derive(
421    Debug,
422    Copy,
423    Clone,
424    Hash,
425    Eq,
426    PartialEq,
427    Ord,
428    PartialOrd,
429    TaskInput,
430    TraceRawVcs,
431    Serialize,
432    Deserialize,
433    NonLocalValue,
434    Encode,
435    Decode,
436)]
437#[serde(rename_all = "kebab-case")]
438pub enum ActionLayer {
439    Rsc,
440    ActionBrowser,
441}
442
443#[derive(Serialize, Debug, Eq, PartialEq, Hash, Clone)]
444#[serde(rename_all = "camelCase")]
445#[serde(untagged)]
446pub enum ModuleId {
447    String(RcStr),
448    Number(u64),
449}
450
451#[derive(Serialize, Default, Debug)]
452#[serde(rename_all = "camelCase")]
453pub struct FontManifest(pub Vec<FontManifestEntry>);
454
455#[derive(Serialize, Default, Debug)]
456#[serde(rename_all = "camelCase")]
457pub struct FontManifestEntry {
458    pub url: RcStr,
459    pub content: RcStr,
460}
461
462#[cfg(test)]
463mod tests {
464    use turbo_rcstr::rcstr;
465
466    use super::*;
467
468    #[test]
469    fn test_middleware_matcher_serialization() {
470        let matchers = vec![
471            ProxyMatcher {
472                regexp: None,
473                locale: false,
474                has: None,
475                missing: None,
476                original_source: rcstr!(""),
477            },
478            ProxyMatcher {
479                regexp: Some(rcstr!(".*")),
480                locale: true,
481                has: Some(vec![RouteHas::Query {
482                    key: rcstr!("foo"),
483                    value: None,
484                }]),
485                missing: Some(vec![RouteHas::Query {
486                    key: rcstr!("bar"),
487                    value: Some(rcstr!("value")),
488                }]),
489                original_source: rcstr!("source"),
490            },
491        ];
492
493        let serialized = serde_json::to_string(&matchers).unwrap();
494        let deserialized: Vec<ProxyMatcher> = serde_json::from_str(&serialized).unwrap();
495
496        assert_eq!(matchers, deserialized);
497    }
498}