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
395#[derive(Serialize, Debug)]
396pub struct ActionManifestWorkerEntry<'a> {
397 #[serde(rename = "moduleId")]
398 pub module_id: ActionManifestModuleId<'a>,
399 #[serde(rename = "async")]
400 pub is_async: bool,
401 #[serde(rename = "exportedName")]
402 pub exported_name: &'a str,
403 pub filename: &'a str,
404}
405
406#[derive(Serialize, Debug, Clone)]
407#[serde(untagged)]
408pub enum ActionManifestModuleId<'a> {
409 String(&'a str),
410 Number(u64),
411}
412
413#[derive(
414 Debug,
415 Copy,
416 Clone,
417 Hash,
418 Eq,
419 PartialEq,
420 Ord,
421 PartialOrd,
422 TaskInput,
423 TraceRawVcs,
424 Serialize,
425 Deserialize,
426 NonLocalValue,
427 Encode,
428 Decode,
429)]
430#[serde(rename_all = "kebab-case")]
431pub enum ActionLayer {
432 Rsc,
433 ActionBrowser,
434}
435
436#[derive(Serialize, Debug, Eq, PartialEq, Hash, Clone)]
437#[serde(rename_all = "camelCase")]
438#[serde(untagged)]
439pub enum ModuleId {
440 String(RcStr),
441 Number(u64),
442}
443
444#[derive(Serialize, Default, Debug)]
445#[serde(rename_all = "camelCase")]
446pub struct FontManifest(pub Vec<FontManifestEntry>);
447
448#[derive(Serialize, Default, Debug)]
449#[serde(rename_all = "camelCase")]
450pub struct FontManifestEntry {
451 pub url: RcStr,
452 pub content: RcStr,
453}
454
455#[cfg(test)]
456mod tests {
457 use turbo_rcstr::rcstr;
458
459 use super::*;
460
461 #[test]
462 fn test_middleware_matcher_serialization() {
463 let matchers = vec![
464 ProxyMatcher {
465 regexp: None,
466 locale: false,
467 has: None,
468 missing: None,
469 original_source: rcstr!(""),
470 },
471 ProxyMatcher {
472 regexp: Some(rcstr!(".*")),
473 locale: true,
474 has: Some(vec![RouteHas::Query {
475 key: rcstr!("foo"),
476 value: None,
477 }]),
478 missing: Some(vec![RouteHas::Query {
479 key: rcstr!("bar"),
480 value: Some(rcstr!("value")),
481 }]),
482 original_source: rcstr!("source"),
483 },
484 ];
485
486 let serialized = serde_json::to_string(&matchers).unwrap();
487 let deserialized: Vec<ProxyMatcher> = serde_json::from_str(&serialized).unwrap();
488
489 assert_eq!(matchers, deserialized);
490 }
491}