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