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.into_iter())
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.into_iter()),
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 matchers: Vec<ProxyMatcher>,
289 pub wasm: Vec<AssetBinding>,
290 pub assets: Vec<AssetBinding>,
291 #[serde(skip_serializing_if = "Option::is_none")]
292 pub regions: Option<Regions>,
293 pub env: FxIndexMap<RcStr, RcStr>,
294}
295
296#[derive(Serialize, Default, Debug)]
297pub struct InstrumentationDefinition {
298 pub files: Vec<RcStr>,
299 pub name: RcStr,
300 #[serde(skip_serializing_if = "Vec::is_empty")]
301 pub wasm: Vec<AssetBinding>,
302 #[serde(skip_serializing_if = "Vec::is_empty")]
303 pub assets: Vec<AssetBinding>,
304}
305
306#[derive(Serialize, Default, Debug)]
307#[serde(rename_all = "camelCase")]
308pub struct AssetBinding {
309 pub name: RcStr,
310 pub file_path: RcStr,
311}
312
313#[derive(Serialize, Debug)]
314#[serde(untagged)]
315pub enum Regions {
316 Multiple(Vec<RcStr>),
317 Single(RcStr),
318}
319
320#[derive(Serialize, Default, Debug)]
321pub struct MiddlewaresManifestV2 {
322 pub sorted_middleware: Vec<RcStr>,
323 pub middleware: FxIndexMap<RcStr, EdgeFunctionDefinition>,
324 pub instrumentation: Option<InstrumentationDefinition>,
325 pub functions: FxIndexMap<RcStr, EdgeFunctionDefinition>,
326}
327
328#[derive(Serialize, Default, Debug)]
329#[serde(rename_all = "camelCase")]
330pub struct ReactLoadableManifest {
331 #[serde(flatten)]
332 pub manifest: FxIndexMap<RcStr, ReactLoadableManifestEntry>,
333}
334
335#[derive(Serialize, Default, Debug)]
336#[serde(rename_all = "camelCase")]
337pub struct ReactLoadableManifestEntry {
338 pub id: u32,
339 pub files: Vec<RcStr>,
340}
341
342#[derive(Serialize, Default, Debug)]
343#[serde(rename_all = "camelCase")]
344pub struct NextFontManifest {
345 pub pages: FxIndexMap<RcStr, Vec<RcStr>>,
346 pub app: FxIndexMap<RcStr, Vec<RcStr>>,
347 pub app_using_size_adjust: bool,
348 pub pages_using_size_adjust: bool,
349}
350
351#[derive(Serialize, Default, Debug)]
352#[serde(rename_all = "camelCase")]
353pub struct AppPathsManifest {
354 #[serde(flatten)]
355 pub edge_server_app_paths: PagesManifest,
356 #[serde(flatten)]
357 pub node_server_app_paths: PagesManifest,
358}
359
360#[derive(Serialize, Debug)]
365#[serde(rename_all = "camelCase")]
366pub struct LoadableManifest {
367 pub id: ModuleId,
368 pub files: Vec<RcStr>,
369}
370
371#[derive(Serialize, Default, Debug)]
372#[serde(rename_all = "camelCase")]
373pub struct ServerReferenceManifest<'a> {
374 pub node: FxIndexMap<&'a str, ActionManifestEntry<'a>>,
376 pub edge: FxIndexMap<&'a str, ActionManifestEntry<'a>>,
378}
379
380#[derive(Serialize, Default, Debug)]
381#[serde(rename_all = "camelCase")]
382pub struct ActionManifestEntry<'a> {
383 pub workers: FxIndexMap<&'a str, ActionManifestWorkerEntry<'a>>,
386
387 pub layer: FxIndexMap<&'a str, ActionLayer>,
388
389 #[serde(rename = "exportedName")]
390 pub exported_name: &'a str,
391
392 pub filename: &'a str,
393
394 #[serde(skip_serializing_if = "Option::is_none")]
396 pub line: Option<u32>,
397
398 #[serde(skip_serializing_if = "Option::is_none")]
400 pub col: Option<u32>,
401}
402
403#[derive(Serialize, Debug)]
404pub struct ActionManifestWorkerEntry<'a> {
405 #[serde(rename = "moduleId")]
406 pub module_id: ActionManifestModuleId<'a>,
407 #[serde(rename = "async")]
408 pub is_async: bool,
409 #[serde(rename = "exportedName")]
410 pub exported_name: &'a str,
411 pub filename: &'a str,
412}
413
414#[derive(Serialize, Debug, Clone)]
415#[serde(untagged)]
416pub enum ActionManifestModuleId<'a> {
417 String(&'a str),
418 Number(u64),
419}
420
421#[derive(
422 Debug,
423 Copy,
424 Clone,
425 Hash,
426 Eq,
427 PartialEq,
428 Ord,
429 PartialOrd,
430 TaskInput,
431 TraceRawVcs,
432 Serialize,
433 Deserialize,
434 NonLocalValue,
435 Encode,
436 Decode,
437)]
438#[serde(rename_all = "kebab-case")]
439pub enum ActionLayer {
440 Rsc,
441 ActionBrowser,
442}
443
444#[derive(Serialize, Debug, Eq, PartialEq, Hash, Clone)]
445#[serde(rename_all = "camelCase")]
446#[serde(untagged)]
447pub enum ModuleId {
448 String(RcStr),
449 Number(u64),
450}
451
452#[derive(Serialize, Default, Debug)]
453#[serde(rename_all = "camelCase")]
454pub struct FontManifest(pub Vec<FontManifestEntry>);
455
456#[derive(Serialize, Default, Debug)]
457#[serde(rename_all = "camelCase")]
458pub struct FontManifestEntry {
459 pub url: RcStr,
460 pub content: RcStr,
461}
462
463#[cfg(test)]
464mod tests {
465 use turbo_rcstr::rcstr;
466
467 use super::*;
468
469 #[test]
470 fn test_middleware_matcher_serialization() {
471 let matchers = vec![
472 ProxyMatcher {
473 regexp: None,
474 locale: false,
475 has: None,
476 missing: None,
477 original_source: rcstr!(""),
478 },
479 ProxyMatcher {
480 regexp: Some(rcstr!(".*")),
481 locale: true,
482 has: Some(vec![RouteHas::Query {
483 key: rcstr!("foo"),
484 value: None,
485 }]),
486 missing: Some(vec![RouteHas::Query {
487 key: rcstr!("bar"),
488 value: Some(rcstr!("value")),
489 }]),
490 original_source: rcstr!("source"),
491 },
492 ];
493
494 let serialized = serde_json::to_string(&matchers).unwrap();
495 let deserialized: Vec<ProxyMatcher> = serde_json::from_str(&serialized).unwrap();
496
497 assert_eq!(matchers, deserialized);
498 }
499}