1pub 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()) .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 #[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#[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 pub node: FxIndexMap<&'a str, ActionManifestEntry<'a>>,
284 pub edge: FxIndexMap<&'a str, ActionManifestEntry<'a>>,
286}
287
288#[derive(Serialize, Default, Debug)]
289#[serde(rename_all = "camelCase")]
290pub struct ActionManifestEntry<'a> {
291 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 pub client_modules: ManifestNode,
341 pub ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
344 #[serde(rename = "edgeSSRModuleMapping")]
346 pub edge_ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
347 pub rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
350 #[serde(rename = "edgeRscModuleMapping")]
352 pub edge_rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
353 #[serde(rename = "entryCSSFiles")]
355 pub entry_css_files: FxIndexMap<RcStr, FxIndexSet<CssResource>>,
356 #[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 #[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 pub id: ModuleId,
389 pub name: RcStr,
391 pub chunks: Vec<RcStr>,
393 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#[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}