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