1pub mod client_reference_manifest;
4mod encode_uri_component;
5
6use anyhow::{Context, Result};
7use bincode::{Decode, Encode};
8use serde::{Deserialize, Serialize};
9use serde_json::value::RawValue;
10use turbo_rcstr::RcStr;
11use turbo_tasks::{
12 FxIndexMap, ReadRef, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc, trace::TraceRawVcs,
13};
14use turbo_tasks_fs::{File, FileContent, FileSystemPath};
15use turbopack_core::{
16 asset::{Asset, AssetContent},
17 output::{OutputAsset, OutputAssets, OutputAssetsReference, OutputAssetsWithReferenced},
18};
19
20use crate::next_config::RouteHas;
21
22#[derive(Serialize, Default, Debug)]
23pub struct PagesManifest {
24 #[serde(flatten)]
25 pub pages: FxIndexMap<RcStr, RcStr>,
26}
27
28#[derive(Debug)]
29#[turbo_tasks::value(shared)]
30pub struct BuildManifest {
31 pub output_path: FileSystemPath,
32 pub client_relative_path: FileSystemPath,
33
34 pub polyfill_files: Vec<ResolvedVc<Box<dyn OutputAsset>>>,
35 pub root_main_files: Vec<ResolvedVc<Box<dyn OutputAsset>>>,
36 #[bincode(with = "turbo_bincode::indexmap")]
37 pub pages: FxIndexMap<RcStr, ResolvedVc<OutputAssets>>,
38 #[bincode(with = "turbo_bincode::indexmap")]
43 pub root_main_files_per_page: FxIndexMap<RcStr, Vec<ResolvedVc<Box<dyn OutputAsset>>>>,
44 #[bincode(with = "turbo_bincode::indexmap")]
47 pub pages_chunk_group_bootstrap_params: FxIndexMap<RcStr, RcStr>,
48 pub chunk_loading_global: RcStr,
50}
51
52#[turbo_tasks::value_impl]
53impl OutputAssetsReference for BuildManifest {
54 #[turbo_tasks::function]
55 async fn references(&self) -> Result<Vc<OutputAssetsWithReferenced>> {
56 let chunks: Vec<ReadRef<OutputAssets>> = self.pages.values().try_join().await?;
57
58 let root_main_files = self
59 .root_main_files
60 .iter()
61 .map(async |c| Ok(c.path().await?.has_extension(".js").then_some(*c)))
62 .try_flat_join()
63 .await?;
64
65 let per_page_files = self
66 .root_main_files_per_page
67 .values()
68 .flatten()
69 .copied()
70 .collect::<Vec<_>>();
71
72 let references = chunks
73 .into_iter()
74 .flatten()
75 .chain(root_main_files)
76 .chain(self.polyfill_files.iter().copied())
77 .chain(per_page_files)
78 .collect();
79
80 Ok(OutputAssetsWithReferenced::from_assets(Vc::cell(
81 references,
82 )))
83 }
84}
85
86#[turbo_tasks::value_impl]
87impl OutputAsset for BuildManifest {
88 #[turbo_tasks::function]
89 async fn path(&self) -> Vc<FileSystemPath> {
90 self.output_path.clone().cell()
91 }
92}
93
94#[turbo_tasks::value_impl]
95impl Asset for BuildManifest {
96 #[turbo_tasks::function]
97 async fn content(&self) -> Result<Vc<AssetContent>> {
98 let client_relative_path = &self.client_relative_path;
99
100 #[derive(Serialize, Default, Debug)]
101 #[serde(rename_all = "camelCase")]
102 pub struct SerializedBuildManifest {
103 pub dev_files: Vec<RcStr>,
104 pub amp_dev_files: Vec<RcStr>,
105 pub polyfill_files: Vec<RcStr>,
106 pub low_priority_files: Vec<RcStr>,
107 pub root_main_files: Vec<RcStr>,
108 pub pages: FxIndexMap<RcStr, Vec<RcStr>>,
109 pub amp_first_pages: Vec<RcStr>,
110 pub root_main_files_tree: FxIndexMap<RcStr, Vec<RcStr>>,
111 pub pages_chunk_group_bootstrap_params: FxIndexMap<RcStr, Box<RawValue>>,
113 pub chunk_loading_global: RcStr,
114 }
115
116 let pages: Vec<(RcStr, Vec<RcStr>)> = self
117 .pages
118 .iter()
119 .map(async |(k, chunks)| {
120 Ok((
121 k.clone(),
122 chunks
123 .await?
124 .iter()
125 .copied()
126 .map(async |chunk| {
127 let chunk_path = chunk.path().await?;
128 Ok(client_relative_path
129 .get_path_to(&chunk_path)
130 .context("client chunk entry path must be inside the client root")?
131 .into())
132 })
133 .try_join()
134 .await?,
135 ))
136 })
137 .try_join()
138 .await?;
139
140 let polyfill_files: Vec<RcStr> = self
141 .polyfill_files
142 .iter()
143 .copied()
144 .map(async |chunk| {
145 let chunk_path = chunk.path().await?;
146 Ok(client_relative_path
147 .get_path_to(&chunk_path)
148 .context("failed to resolve client-relative path to polyfill")?
149 .into())
150 })
151 .try_join()
152 .await?;
153
154 let root_main_files: Vec<RcStr> = self
155 .root_main_files
156 .iter()
157 .map(async |chunk| {
158 let chunk_path = chunk.path().await?;
159 if !chunk_path.has_extension(".js") {
160 Ok(None)
161 } else {
162 Ok(Some(
163 client_relative_path
164 .get_path_to(&chunk_path)
165 .context("failed to resolve client-relative path to root_main_file")?
166 .into(),
167 ))
168 }
169 })
170 .try_flat_join()
171 .await?;
172
173 let root_main_files_tree: Vec<(RcStr, Vec<RcStr>)> = self
174 .root_main_files_per_page
175 .iter()
176 .map(async |(page, per_page_chunks)| {
177 let per_page_paths: Vec<RcStr> = per_page_chunks
178 .iter()
179 .copied()
180 .map(async |chunk| {
181 let chunk_path = chunk.path().await?;
182 Ok(client_relative_path
183 .get_path_to(&chunk_path)
184 .context(
185 "failed to resolve client-relative path to per-page root file",
186 )?
187 .into())
188 })
189 .try_join()
190 .await?;
191 let combined = root_main_files
194 .iter()
195 .cloned()
196 .chain(per_page_paths)
197 .collect();
198 Ok((page.clone(), combined))
199 })
200 .try_join()
201 .await?;
202
203 let manifest = SerializedBuildManifest {
204 pages: FxIndexMap::from_iter(pages),
205 polyfill_files,
206 root_main_files,
207 root_main_files_tree: FxIndexMap::from_iter(root_main_files_tree),
208 pages_chunk_group_bootstrap_params: self
209 .pages_chunk_group_bootstrap_params
210 .iter()
211 .map(|(k, v)| Ok((k.clone(), RawValue::from_string(v.to_string())?)))
212 .collect::<Result<FxIndexMap<_, _>>>()?,
213 chunk_loading_global: self.chunk_loading_global.clone(),
214 ..Default::default()
215 };
216
217 Ok(AssetContent::file(
218 FileContent::Content(File::from(serde_json::to_string_pretty(&manifest)?)).cell(),
219 ))
220 }
221}
222
223#[derive(Debug)]
224#[turbo_tasks::value(shared)]
225pub struct ClientBuildManifest {
226 pub output_path: FileSystemPath,
227 pub client_relative_path: FileSystemPath,
228
229 #[bincode(with = "turbo_bincode::indexmap")]
230 pub pages: FxIndexMap<RcStr, ResolvedVc<Box<dyn OutputAsset>>>,
231}
232
233#[turbo_tasks::value_impl]
234impl OutputAssetsReference for ClientBuildManifest {
235 #[turbo_tasks::function]
236 async fn references(&self) -> Result<Vc<OutputAssetsWithReferenced>> {
237 let chunks: Vec<ResolvedVc<Box<dyn OutputAsset>>> = self.pages.values().copied().collect();
238 Ok(OutputAssetsWithReferenced::from_assets(Vc::cell(chunks)))
239 }
240}
241
242#[turbo_tasks::value_impl]
243impl OutputAsset for ClientBuildManifest {
244 #[turbo_tasks::function]
245 async fn path(&self) -> Vc<FileSystemPath> {
246 self.output_path.clone().cell()
247 }
248}
249
250#[turbo_tasks::value_impl]
251impl Asset for ClientBuildManifest {
252 #[turbo_tasks::function]
253 async fn content(&self) -> Result<Vc<AssetContent>> {
254 let client_relative_path = &self.client_relative_path;
255
256 let manifest: FxIndexMap<RcStr, Vec<RcStr>> = self
257 .pages
258 .iter()
259 .map(async |(k, chunk)| {
260 Ok((
261 k.clone(),
262 vec![
263 client_relative_path
264 .get_path_to(&*chunk.path().await?)
265 .context("client chunk entry path must be inside the client root")?
266 .into(),
267 ],
268 ))
269 })
270 .try_join()
271 .await?
272 .into_iter()
273 .collect();
274
275 Ok(AssetContent::file(
276 FileContent::Content(File::from(serde_json::to_string_pretty(&manifest)?)).cell(),
277 ))
278 }
279}
280
281#[derive(Serialize, Debug)]
282#[serde(rename_all = "camelCase", tag = "version")]
283#[allow(clippy::large_enum_variant)]
284pub enum MiddlewaresManifest {
285 #[serde(rename = "2")]
286 MiddlewaresManifestV2(MiddlewaresManifestV2),
287 #[serde(other)]
288 Unsupported,
289}
290
291impl Default for MiddlewaresManifest {
292 fn default() -> Self {
293 Self::MiddlewaresManifestV2(Default::default())
294 }
295}
296
297#[turbo_tasks::task_input]
298#[derive(
299 Debug,
300 Clone,
301 Hash,
302 Eq,
303 PartialEq,
304 Ord,
305 PartialOrd,
306 TraceRawVcs,
307 Serialize,
308 Deserialize,
309 Encode,
310 Decode,
311)]
312#[serde(rename_all = "camelCase", default)]
313pub struct ProxyMatcher {
314 #[serde(skip_serializing_if = "Option::is_none")]
316 pub regexp: Option<RcStr>,
317 #[serde(skip_serializing_if = "bool_is_true")]
318 pub locale: bool,
319 #[serde(skip_serializing_if = "Option::is_none")]
320 pub has: Option<Vec<RouteHas>>,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 pub missing: Option<Vec<RouteHas>>,
323 pub original_source: RcStr,
324}
325
326impl Default for ProxyMatcher {
327 fn default() -> Self {
328 Self {
329 regexp: None,
330 locale: true,
331 has: None,
332 missing: None,
333 original_source: Default::default(),
334 }
335 }
336}
337
338fn bool_is_true(b: &bool) -> bool {
339 *b
340}
341
342#[derive(Serialize, Default, Debug)]
343pub struct EdgeFunctionDefinition {
344 pub files: Vec<RcStr>,
345 pub name: RcStr,
346 pub page: RcStr,
347 pub entrypoint: RcStr,
348 pub matchers: Vec<ProxyMatcher>,
349 pub wasm: Vec<AssetBinding>,
350 pub assets: Vec<AssetBinding>,
351 #[serde(skip_serializing_if = "Option::is_none")]
352 pub regions: Option<Regions>,
353 pub env: FxIndexMap<RcStr, RcStr>,
354}
355
356#[derive(Serialize, Default, Debug)]
357pub struct InstrumentationDefinition {
358 pub files: Vec<RcStr>,
359 pub name: RcStr,
360 #[serde(skip_serializing_if = "Vec::is_empty")]
361 pub wasm: Vec<AssetBinding>,
362 #[serde(skip_serializing_if = "Vec::is_empty")]
363 pub assets: Vec<AssetBinding>,
364}
365
366#[derive(Serialize, Default, Debug)]
367#[serde(rename_all = "camelCase")]
368pub struct AssetBinding {
369 pub name: RcStr,
370 pub file_path: RcStr,
371}
372
373#[derive(Serialize, Debug)]
374#[serde(untagged)]
375pub enum Regions {
376 Multiple(Vec<RcStr>),
377 Single(RcStr),
378}
379
380#[derive(Serialize, Default, Debug)]
381pub struct MiddlewaresManifestV2 {
382 pub sorted_middleware: Vec<RcStr>,
383 pub middleware: FxIndexMap<RcStr, EdgeFunctionDefinition>,
384 pub instrumentation: Option<InstrumentationDefinition>,
385 pub functions: FxIndexMap<RcStr, EdgeFunctionDefinition>,
386}
387
388#[derive(Serialize, Default, Debug)]
389#[serde(rename_all = "camelCase")]
390pub struct ReactLoadableManifest {
391 #[serde(flatten)]
392 pub manifest: FxIndexMap<RcStr, ReactLoadableManifestEntry>,
393}
394
395#[derive(Serialize, Default, Debug)]
396#[serde(rename_all = "camelCase")]
397pub struct ReactLoadableManifestEntry {
398 pub id: u32,
399 pub files: Vec<RcStr>,
400}
401
402#[derive(Serialize, Default, Debug)]
403#[serde(rename_all = "camelCase")]
404pub struct NextFontManifest {
405 pub pages: FxIndexMap<RcStr, Vec<RcStr>>,
406 pub app: FxIndexMap<RcStr, Vec<RcStr>>,
407 pub app_using_size_adjust: bool,
408 pub pages_using_size_adjust: bool,
409}
410
411#[derive(Serialize, Default, Debug)]
412#[serde(rename_all = "camelCase")]
413pub struct AppPathsManifest {
414 #[serde(flatten)]
415 pub edge_server_app_paths: PagesManifest,
416 #[serde(flatten)]
417 pub node_server_app_paths: PagesManifest,
418}
419
420#[derive(Serialize, Debug)]
425#[serde(rename_all = "camelCase")]
426pub struct LoadableManifest {
427 pub id: ModuleId,
428 pub files: Vec<RcStr>,
429}
430
431#[derive(Serialize, Default, Debug)]
432#[serde(rename_all = "camelCase")]
433pub struct ServerReferenceManifest<'a> {
434 pub node: FxIndexMap<&'a str, ActionManifestEntry<'a>>,
436 pub edge: FxIndexMap<&'a str, ActionManifestEntry<'a>>,
438}
439
440#[derive(Serialize, Default, Debug)]
441#[serde(rename_all = "camelCase")]
442pub struct ActionManifestEntry<'a> {
443 pub workers: FxIndexMap<&'a str, ActionManifestWorkerEntry<'a>>,
446
447 #[serde(rename = "exportedName")]
448 pub exported_name: &'a str,
449
450 pub filename: &'a str,
451
452 #[serde(skip_serializing_if = "Option::is_none")]
454 pub line: Option<u32>,
455
456 #[serde(skip_serializing_if = "Option::is_none")]
458 pub col: Option<u32>,
459}
460
461#[derive(Serialize, Debug)]
462pub struct ActionManifestWorkerEntry<'a> {
463 #[serde(rename = "moduleId")]
464 pub module_id: ActionManifestModuleId<'a>,
465 #[serde(rename = "async")]
466 pub is_async: bool,
467 #[serde(rename = "codeHash")]
468 pub code_hash: Option<&'a str>,
469}
470
471#[derive(Serialize, Debug, Clone)]
472#[serde(untagged)]
473pub enum ActionManifestModuleId<'a> {
474 String(&'a str),
475 Number(u64),
476}
477
478#[turbo_tasks::task_input]
479#[derive(
480 Debug,
481 Copy,
482 Clone,
483 Hash,
484 Eq,
485 PartialEq,
486 Ord,
487 PartialOrd,
488 TraceRawVcs,
489 Serialize,
490 Deserialize,
491 Encode,
492 Decode,
493)]
494#[serde(rename_all = "kebab-case")]
495pub enum ActionLayer {
496 Rsc,
497 ActionBrowser,
498}
499
500#[derive(Serialize, Debug, Eq, PartialEq, Hash, Clone)]
501#[serde(rename_all = "camelCase")]
502#[serde(untagged)]
503pub enum ModuleId {
504 String(RcStr),
505 Number(u64),
506}
507
508#[derive(Serialize, Default, Debug)]
509#[serde(rename_all = "camelCase")]
510pub struct FontManifest(pub Vec<FontManifestEntry>);
511
512#[derive(Serialize, Default, Debug)]
513#[serde(rename_all = "camelCase")]
514pub struct FontManifestEntry {
515 pub url: RcStr,
516 pub content: RcStr,
517}
518
519#[cfg(test)]
520mod tests {
521 use turbo_rcstr::rcstr;
522
523 use super::*;
524
525 #[test]
526 fn test_middleware_matcher_serialization() {
527 let matchers = vec![
528 ProxyMatcher {
529 regexp: None,
530 locale: false,
531 has: None,
532 missing: None,
533 original_source: rcstr!(""),
534 },
535 ProxyMatcher {
536 regexp: Some(rcstr!(".*")),
537 locale: true,
538 has: Some(vec![RouteHas::Query {
539 key: rcstr!("foo"),
540 value: None,
541 }]),
542 missing: Some(vec![RouteHas::Query {
543 key: rcstr!("bar"),
544 value: Some(rcstr!("value")),
545 }]),
546 original_source: rcstr!("source"),
547 },
548 ];
549
550 let serialized = serde_json::to_string(&matchers).unwrap();
551 let deserialized: Vec<ProxyMatcher> = serde_json::from_str(&serialized).unwrap();
552
553 assert_eq!(matchers, deserialized);
554 }
555}