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: 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()) .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 #[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#[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 pub node: FxIndexMap<&'a str, ActionManifestEntry<'a>>,
302 pub edge: FxIndexMap<&'a str, ActionManifestEntry<'a>>,
304}
305
306#[derive(Serialize, Default, Debug)]
307#[serde(rename_all = "camelCase")]
308pub struct ActionManifestEntry<'a> {
309 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 pub client_modules: ManifestNode,
359 pub ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
362 #[serde(rename = "edgeSSRModuleMapping")]
364 pub edge_ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
365 pub rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
368 #[serde(rename = "edgeRscModuleMapping")]
370 pub edge_rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
371 #[serde(rename = "entryCSSFiles")]
373 pub entry_css_files: FxIndexMap<RcStr, FxIndexSet<CssResource>>,
374 #[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 #[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 pub id: ModuleId,
407 pub name: RcStr,
409 pub chunks: Vec<RcStr>,
411 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#[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}