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