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