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