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
395#[derive(Serialize, Debug)]
396pub struct ActionManifestWorkerEntry<'a> {
397    #[serde(rename = "moduleId")]
398    pub module_id: ActionManifestModuleId<'a>,
399    #[serde(rename = "async")]
400    pub is_async: bool,
401    #[serde(rename = "exportedName")]
402    pub exported_name: &'a str,
403    pub filename: &'a str,
404}
405
406#[derive(Serialize, Debug, Clone)]
407#[serde(untagged)]
408pub enum ActionManifestModuleId<'a> {
409    String(&'a str),
410    Number(u64),
411}
412
413#[derive(
414    Debug,
415    Copy,
416    Clone,
417    Hash,
418    Eq,
419    PartialEq,
420    Ord,
421    PartialOrd,
422    TaskInput,
423    TraceRawVcs,
424    Serialize,
425    Deserialize,
426    NonLocalValue,
427    Encode,
428    Decode,
429)]
430#[serde(rename_all = "kebab-case")]
431pub enum ActionLayer {
432    Rsc,
433    ActionBrowser,
434}
435
436#[derive(Serialize, Debug, Eq, PartialEq, Hash, Clone)]
437#[serde(rename_all = "camelCase")]
438#[serde(untagged)]
439pub enum ModuleId {
440    String(RcStr),
441    Number(u64),
442}
443
444#[derive(Serialize, Default, Debug)]
445#[serde(rename_all = "camelCase")]
446pub struct FontManifest(pub Vec<FontManifestEntry>);
447
448#[derive(Serialize, Default, Debug)]
449#[serde(rename_all = "camelCase")]
450pub struct FontManifestEntry {
451    pub url: RcStr,
452    pub content: RcStr,
453}
454
455#[cfg(test)]
456mod tests {
457    use turbo_rcstr::rcstr;
458
459    use super::*;
460
461    #[test]
462    fn test_middleware_matcher_serialization() {
463        let matchers = vec![
464            ProxyMatcher {
465                regexp: None,
466                locale: false,
467                has: None,
468                missing: None,
469                original_source: rcstr!(""),
470            },
471            ProxyMatcher {
472                regexp: Some(rcstr!(".*")),
473                locale: true,
474                has: Some(vec![RouteHas::Query {
475                    key: rcstr!("foo"),
476                    value: None,
477                }]),
478                missing: Some(vec![RouteHas::Query {
479                    key: rcstr!("bar"),
480                    value: Some(rcstr!("value")),
481                }]),
482                original_source: rcstr!("source"),
483            },
484        ];
485
486        let serialized = serde_json::to_string(&matchers).unwrap();
487        let deserialized: Vec<ProxyMatcher> = serde_json::from_str(&serialized).unwrap();
488
489        assert_eq!(matchers, deserialized);
490    }
491}