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