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