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    #[serde(rename = "exportedName")]
316    pub exported_name: &'a str,
317
318    pub filename: &'a str,
319}
320
321#[derive(Serialize, Debug)]
322pub struct ActionManifestWorkerEntry<'a> {
323    #[serde(rename = "moduleId")]
324    pub module_id: ActionManifestModuleId<'a>,
325    #[serde(rename = "async")]
326    pub is_async: bool,
327    #[serde(rename = "exportedName")]
328    pub exported_name: &'a str,
329    pub filename: &'a str,
330}
331
332#[derive(Serialize, Debug, Clone)]
333#[serde(untagged)]
334pub enum ActionManifestModuleId<'a> {
335    String(&'a str),
336    Number(u64),
337}
338
339#[derive(
340    Debug,
341    Copy,
342    Clone,
343    Hash,
344    Eq,
345    PartialEq,
346    Ord,
347    PartialOrd,
348    TaskInput,
349    TraceRawVcs,
350    Serialize,
351    Deserialize,
352    NonLocalValue,
353)]
354#[serde(rename_all = "kebab-case")]
355pub enum ActionLayer {
356    Rsc,
357    ActionBrowser,
358}
359
360#[derive(Serialize, Default, Debug)]
361#[serde(rename_all = "camelCase")]
362pub struct ClientReferenceManifest {
363    pub module_loading: ModuleLoading,
364    /// Mapping of module path and export name to client module ID and required
365    /// client chunks.
366    pub client_modules: ManifestNode,
367    /// Mapping of client module ID to corresponding SSR module ID and required
368    /// SSR chunks.
369    pub ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
370    /// Same as `ssr_module_mapping`, but for Edge SSR.
371    #[serde(rename = "edgeSSRModuleMapping")]
372    pub edge_ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
373    /// Mapping of client module ID to corresponding RSC module ID and required
374    /// RSC chunks.
375    pub rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
376    /// Same as `rsc_module_mapping`, but for Edge RSC.
377    #[serde(rename = "edgeRscModuleMapping")]
378    pub edge_rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
379    /// Mapping of server component path to required CSS client chunks.
380    #[serde(rename = "entryCSSFiles")]
381    pub entry_css_files: FxIndexMap<RcStr, FxIndexSet<CssResource>>,
382    /// Mapping of server component path to required JS client chunks.
383    #[serde(rename = "entryJSFiles")]
384    pub entry_js_files: FxIndexMap<RcStr, FxIndexSet<RcStr>>,
385}
386
387#[derive(Serialize, Debug, Clone, Eq, Hash, PartialEq)]
388pub struct CssResource {
389    pub path: RcStr,
390    pub inlined: bool,
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub content: Option<RcStr>,
393}
394
395#[derive(Serialize, Default, Debug)]
396#[serde(rename_all = "camelCase")]
397pub struct ModuleLoading {
398    pub prefix: RcStr,
399    pub cross_origin: Option<CrossOriginConfig>,
400}
401
402#[derive(Serialize, Default, Debug, Clone)]
403#[serde(rename_all = "camelCase")]
404pub struct ManifestNode {
405    /// Mapping of export name to manifest node entry.
406    #[serde(flatten)]
407    pub module_exports: FxIndexMap<RcStr, ManifestNodeEntry>,
408}
409
410#[derive(Serialize, Debug, Clone)]
411#[serde(rename_all = "camelCase")]
412pub struct ManifestNodeEntry {
413    /// Turbopack module ID.
414    pub id: ModuleId,
415    /// Export name.
416    pub name: RcStr,
417    /// Chunks for the module. JS and CSS.
418    pub chunks: Vec<RcStr>,
419    // TODO(WEB-434)
420    pub r#async: bool,
421}
422
423#[derive(Serialize, Debug, Eq, PartialEq, Hash, Clone)]
424#[serde(rename_all = "camelCase")]
425#[serde(untagged)]
426pub enum ModuleId {
427    String(RcStr),
428    Number(u64),
429}
430
431#[derive(Serialize, Default, Debug)]
432#[serde(rename_all = "camelCase")]
433pub struct FontManifest(pub Vec<FontManifestEntry>);
434
435#[derive(Serialize, Default, Debug)]
436#[serde(rename_all = "camelCase")]
437pub struct FontManifestEntry {
438    pub url: RcStr,
439    pub content: RcStr,
440}
441
442#[derive(Default, Debug)]
443pub struct AppBuildManifest {
444    pub pages: FxIndexMap<RcStr, ResolvedVc<OutputAssets>>,
445}
446
447impl AppBuildManifest {
448    pub async fn build_output(
449        self,
450        output_path: FileSystemPath,
451        client_relative_path: FileSystemPath,
452    ) -> Result<Vc<Box<dyn OutputAsset>>> {
453        let client_relative_path_ref = client_relative_path.clone();
454
455        #[derive(Serialize)]
456        #[serde(rename_all = "camelCase")]
457        pub struct SerializedAppBuildManifest {
458            pub pages: FxIndexMap<RcStr, Vec<RcStr>>,
459        }
460
461        let pages: Vec<(RcStr, Vec<RcStr>)> = self
462            .pages
463            .iter()
464            .map(|(k, chunks)| {
465                let client_relative_path_ref = client_relative_path_ref.clone();
466
467                async move {
468                    Ok((
469                        k.clone(),
470                        chunks
471                            .await?
472                            .iter()
473                            .copied()
474                            .map(|chunk| {
475                                let client_relative_path_ref = client_relative_path_ref.clone();
476
477                                async move {
478                                    let chunk_path = chunk.path().await?;
479                                    Ok(client_relative_path_ref
480                                        .get_path_to(&chunk_path)
481                                        .context(
482                                            "client chunk entry path must be inside the client \
483                                             root",
484                                        )?
485                                        .into())
486                                }
487                            })
488                            .try_join()
489                            .await?,
490                    ))
491                }
492            })
493            .try_join()
494            .await?;
495
496        let manifest = SerializedAppBuildManifest {
497            pages: FxIndexMap::from_iter(pages.into_iter()),
498        };
499
500        let references = self.pages.values().try_join().await?;
501
502        let references = references
503            .into_iter()
504            .flat_map(|c| c.into_iter().copied())
505            .collect();
506
507        Ok(Vc::upcast(VirtualOutputAsset::new_with_references(
508            output_path,
509            AssetContent::file(File::from(serde_json::to_string_pretty(&manifest)?).into()),
510            Vc::cell(references),
511        )))
512    }
513}
514
515// TODO(alexkirsz) Unify with the one for dev.
516#[derive(Serialize, Debug)]
517#[serde(rename_all = "camelCase")]
518pub struct ClientBuildManifest<'a> {
519    #[serde(rename = "__rewrites")]
520    pub rewrites: &'a Rewrites,
521
522    pub sorted_pages: &'a [RcStr],
523
524    #[serde(flatten)]
525    pub pages: FxIndexMap<RcStr, Vec<&'a str>>,
526}
527
528#[cfg(test)]
529mod tests {
530    use turbo_rcstr::rcstr;
531
532    use super::*;
533
534    #[test]
535    fn test_middleware_matcher_serialization() {
536        let matchers = vec![
537            MiddlewareMatcher {
538                regexp: None,
539                locale: false,
540                has: None,
541                missing: None,
542                original_source: rcstr!(""),
543            },
544            MiddlewareMatcher {
545                regexp: Some(rcstr!(".*")),
546                locale: true,
547                has: Some(vec![RouteHas::Query {
548                    key: rcstr!("foo"),
549                    value: None,
550                }]),
551                missing: Some(vec![RouteHas::Query {
552                    key: rcstr!("bar"),
553                    value: Some(rcstr!("value")),
554                }]),
555                original_source: rcstr!("source"),
556            },
557        ];
558
559        let serialized = serde_json::to_string(&matchers).unwrap();
560        let deserialized: Vec<MiddlewareMatcher> = serde_json::from_str(&serialized).unwrap();
561
562        assert_eq!(matchers, deserialized);
563    }
564}