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