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 #[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 pub client_modules: ManifestNode,
367 pub ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
370 #[serde(rename = "edgeSSRModuleMapping")]
372 pub edge_ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
373 pub rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
376 #[serde(rename = "edgeRscModuleMapping")]
378 pub edge_rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
379 #[serde(rename = "entryCSSFiles")]
381 pub entry_css_files: FxIndexMap<RcStr, FxIndexSet<CssResource>>,
382 #[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 #[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 pub id: ModuleId,
415 pub name: RcStr,
417 pub chunks: Vec<RcStr>,
419 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#[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}