1pub mod app_client_references_chunks;
2pub mod app_client_shared_chunks;
3pub mod app_entry;
4pub mod app_page_entry;
5pub mod app_route_entry;
6pub mod metadata;
7
8use std::{
9 cmp::Ordering,
10 fmt::{Display, Formatter, Write},
11 ops::Deref,
12};
13
14use anyhow::{Result, bail};
15use bincode::{Decode, Encode};
16use turbo_rcstr::RcStr;
17use turbo_tasks::{NonLocalValue, TaskInput, trace::TraceRawVcs};
18
19pub use crate::next_app::{
20 app_client_references_chunks::{ClientReferencesChunks, get_app_client_references_chunks},
21 app_client_shared_chunks::get_app_client_shared_chunk_group,
22 app_entry::AppEntry,
23 app_page_entry::get_app_page_entry,
24 app_route_entry::get_app_route_entry,
25};
26
27#[derive(
29 Clone,
30 Debug,
31 Hash,
32 PartialEq,
33 Eq,
34 PartialOrd,
35 Ord,
36 TaskInput,
37 TraceRawVcs,
38 NonLocalValue,
39 Encode,
40 Decode,
41)]
42pub enum PageSegment {
43 Static(RcStr),
45 Dynamic(RcStr),
47 CatchAll(RcStr),
49 OptionalCatchAll(RcStr),
51 Group(RcStr),
53 Parallel(RcStr),
55 PageType(PageType),
58}
59
60impl PageSegment {
61 pub fn parse(segment: &str) -> Result<Self> {
62 if segment.is_empty() {
63 bail!("empty segments are not allowed");
64 }
65
66 if segment.contains('/') {
67 bail!("slashes are not allowed in segments");
68 }
69
70 if let Some(s) = segment.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
71 return Ok(PageSegment::Group(s.into()));
72 }
73
74 if let Some(s) = segment.strip_prefix('@') {
75 return Ok(PageSegment::Parallel(s.into()));
76 }
77
78 if let Some(s) = segment
79 .strip_prefix("[[...")
80 .and_then(|s| s.strip_suffix("]]"))
81 {
82 return Ok(PageSegment::OptionalCatchAll(s.into()));
83 }
84
85 if let Some(s) = segment
86 .strip_prefix("[...")
87 .and_then(|s| s.strip_suffix(']'))
88 {
89 return Ok(PageSegment::CatchAll(s.into()));
90 }
91
92 if let Some(s) = segment.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
93 return Ok(PageSegment::Dynamic(s.into()));
94 }
95
96 Ok(PageSegment::Static(segment.into()))
97 }
98}
99
100impl Display for PageSegment {
101 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
102 match self {
103 PageSegment::Static(s) => f.write_str(s),
104 PageSegment::Dynamic(s) => {
105 f.write_char('[')?;
106 f.write_str(s)?;
107 f.write_char(']')
108 }
109 PageSegment::CatchAll(s) => {
110 f.write_str("[...")?;
111 f.write_str(s)?;
112 f.write_char(']')
113 }
114 PageSegment::OptionalCatchAll(s) => {
115 f.write_str("[[...")?;
116 f.write_str(s)?;
117 f.write_str("]]")
118 }
119 PageSegment::Group(s) => {
120 f.write_char('(')?;
121 f.write_str(s)?;
122 f.write_char(')')
123 }
124 PageSegment::Parallel(s) => {
125 f.write_char('@')?;
126 f.write_str(s)
127 }
128 PageSegment::PageType(s) => Display::fmt(s, f),
129 }
130 }
131}
132
133#[derive(
134 Clone,
135 Debug,
136 Hash,
137 PartialEq,
138 Eq,
139 PartialOrd,
140 Ord,
141 TaskInput,
142 TraceRawVcs,
143 NonLocalValue,
144 Encode,
145 Decode,
146)]
147pub enum PageType {
148 Page,
149 Route,
150}
151
152impl Display for PageType {
153 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
154 f.write_str(match self {
155 PageType::Page => "page",
156 PageType::Route => "route",
157 })
158 }
159}
160
161#[derive(
165 Clone,
166 Debug,
167 Hash,
168 PartialEq,
169 Eq,
170 Default,
171 TaskInput,
172 TraceRawVcs,
173 NonLocalValue,
174 Encode,
175 Decode,
176)]
177pub struct AppPage(pub Vec<PageSegment>);
178
179impl AppPage {
180 pub fn new() -> Self {
181 Self::default()
182 }
183
184 pub fn push(&mut self, segment: PageSegment) -> Result<()> {
185 let has_catchall = self.0.iter().any(|segment| {
186 matches!(
187 segment,
188 PageSegment::CatchAll(..) | PageSegment::OptionalCatchAll(..)
189 )
190 });
191
192 if has_catchall
193 && matches!(
194 segment,
195 PageSegment::Static(..)
196 | PageSegment::Dynamic(..)
197 | PageSegment::CatchAll(..)
198 | PageSegment::OptionalCatchAll(..)
199 )
200 {
201 bail!(
202 "Invalid segment {:?}, catch all segment must be the last segment modifying the \
203 path (segments: {:?})",
204 segment,
205 self.0
206 )
207 }
208
209 if self.is_complete() {
210 bail!(
211 "Invalid segment {:?}, this page path already has the final PageType appended \
212 (segments: {:?})",
213 segment,
214 self.0
215 )
216 }
217
218 self.0.push(segment);
219 Ok(())
220 }
221
222 pub fn push_str(&mut self, segment: &str) -> Result<()> {
223 if segment.is_empty() {
224 return Ok(());
225 }
226
227 self.push(PageSegment::parse(segment)?)
228 }
229
230 pub fn clone_push(&self, segment: PageSegment) -> Result<Self> {
231 let mut cloned = self.clone();
232 cloned.push(segment)?;
233 Ok(cloned)
234 }
235
236 pub fn clone_push_str(&self, segment: &str) -> Result<Self> {
237 let mut cloned = self.clone();
238 cloned.push_str(segment)?;
239 Ok(cloned)
240 }
241
242 pub fn parse(page: &str) -> Result<Self> {
243 let mut app_page = Self::new();
244
245 for segment in page.split('/') {
246 app_page.push_str(segment)?;
247 }
248
249 if let Some(last) = app_page.0.last_mut()
250 && let PageSegment::Static(last_name) = &*last
251 {
252 if last_name == "page" || last_name.starts_with("page.") {
254 *last = PageSegment::PageType(PageType::Page);
255 } else if last_name == "route" || last_name.starts_with("route.") {
256 *last = PageSegment::PageType(PageType::Route);
257 }
258 }
260
261 Ok(app_page)
262 }
263
264 pub fn is_root(&self) -> bool {
265 self.0.is_empty()
266 }
267
268 pub fn is_complete(&self) -> bool {
269 matches!(self.0.last(), Some(PageSegment::PageType(..)))
270 }
271
272 pub fn get_last_routing_segment(&self) -> Option<&PageSegment> {
276 self.0.iter().rev().find(|segment| {
277 !matches!(
278 segment,
279 PageSegment::PageType(_) | PageSegment::Group(_) | PageSegment::Parallel(_)
280 )
281 })
282 }
283
284 pub fn is_catchall(&self) -> bool {
285 matches!(
286 self.get_last_routing_segment(),
287 Some(PageSegment::CatchAll(_) | PageSegment::OptionalCatchAll(_))
288 )
289 }
290
291 pub fn is_intercepting(&self) -> bool {
292 let segment = if self.is_complete() {
293 self.0.iter().nth_back(1)
295 } else {
296 self.0.last()
297 };
298
299 matches!(
300 segment,
301 Some(PageSegment::Static(segment))
302 if segment.starts_with("(.)")
303 || segment.starts_with("(..)")
304 || segment.starts_with("(...)")
305 )
306 }
307
308 pub fn is_first_layer_group_route(&self) -> bool {
310 self.0.len() == 1 && matches!(self.0.last(), Some(PageSegment::Group(_)))
311 }
312
313 pub fn complete(&self, page_type: PageType) -> Result<Self> {
314 self.clone_push(PageSegment::PageType(page_type))
315 }
316}
317
318impl Display for AppPage {
319 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
320 if self.0.is_empty() {
321 return f.write_char('/');
322 }
323
324 for segment in &self.0 {
325 f.write_char('/')?;
326 Display::fmt(segment, f)?;
327 }
328
329 Ok(())
330 }
331}
332
333impl Deref for AppPage {
334 type Target = [PageSegment];
335
336 fn deref(&self) -> &Self::Target {
337 &self.0
338 }
339}
340
341impl Ord for AppPage {
342 fn cmp(&self, other: &Self) -> Ordering {
343 self.len().cmp(&other.len()).then(other.0.cmp(&self.0))
347 }
348}
349
350impl PartialOrd for AppPage {
351 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
352 Some(self.cmp(other))
353 }
354}
355
356#[derive(
360 Clone,
361 Debug,
362 Hash,
363 PartialEq,
364 Eq,
365 PartialOrd,
366 Ord,
367 TaskInput,
368 TraceRawVcs,
369 NonLocalValue,
370 Encode,
371 Decode,
372)]
373pub enum PathSegment {
374 Static(RcStr),
376 Dynamic(RcStr),
378 CatchAll(RcStr),
380 OptionalCatchAll(RcStr),
382}
383
384impl Display for PathSegment {
385 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
386 match self {
387 PathSegment::Static(s) => f.write_str(s),
388 PathSegment::Dynamic(s) => {
389 f.write_char('[')?;
390 f.write_str(s)?;
391 f.write_char(']')
392 }
393 PathSegment::CatchAll(s) => {
394 f.write_str("[...")?;
395 f.write_str(s)?;
396 f.write_char(']')
397 }
398 PathSegment::OptionalCatchAll(s) => {
399 f.write_str("[[...")?;
400 f.write_str(s)?;
401 f.write_str("]]")
402 }
403 }
404 }
405}
406
407#[derive(
413 Clone,
414 Debug,
415 Hash,
416 PartialEq,
417 Eq,
418 Default,
419 TaskInput,
420 TraceRawVcs,
421 NonLocalValue,
422 Encode,
423 Decode,
424)]
425pub struct AppPath(pub Vec<PathSegment>);
426
427impl AppPath {
428 pub fn is_dynamic(&self) -> bool {
429 self.iter().any(|segment| {
430 matches!(
431 (segment,),
432 (PathSegment::Dynamic(_)
433 | PathSegment::CatchAll(_)
434 | PathSegment::OptionalCatchAll(_),)
435 )
436 })
437 }
438
439 pub fn is_root(&self) -> bool {
440 self.0.is_empty()
441 }
442
443 pub fn is_catchall(&self) -> bool {
444 matches!(
446 self.last(),
447 Some(PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_))
448 )
449 }
450
451 pub fn contains(&self, other: &AppPath) -> bool {
452 for (i, segment) in other.0.iter().enumerate() {
454 let Some(self_segment) = self.0.get(i) else {
455 return false;
457 };
458
459 if self_segment == segment {
460 continue;
461 }
462
463 if matches!(
464 segment,
465 PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_)
466 ) {
467 return true;
468 }
469
470 return false;
471 }
472
473 true
474 }
475
476 pub fn contains_interception(&self) -> bool {
480 self.iter().any(|segment| {
481 matches!(
482 segment,
483 PathSegment::Static(s) if s.starts_with("(.)") || s.starts_with("(..)") || s.starts_with("(...)")
484 )
485 })
486 }
487}
488
489impl Deref for AppPath {
490 type Target = [PathSegment];
491
492 fn deref(&self) -> &Self::Target {
493 &self.0
494 }
495}
496
497impl Display for AppPath {
498 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
499 if self.0.is_empty() {
500 return f.write_char('/');
501 }
502
503 for segment in &self.0 {
504 f.write_char('/')?;
505 Display::fmt(segment, f)?;
506 }
507
508 Ok(())
509 }
510}
511
512impl Ord for AppPath {
513 fn cmp(&self, other: &Self) -> Ordering {
514 self.len().cmp(&other.len()).then(other.0.cmp(&self.0))
518 }
519}
520
521impl PartialOrd for AppPath {
522 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
523 Some(self.cmp(other))
524 }
525}
526
527impl From<AppPage> for AppPath {
528 fn from(value: AppPage) -> Self {
529 AppPath(
530 value
531 .0
532 .into_iter()
533 .filter_map(|segment| match segment {
534 PageSegment::Static(s) => Some(PathSegment::Static(s)),
535 PageSegment::Dynamic(s) => Some(PathSegment::Dynamic(s)),
536 PageSegment::CatchAll(s) => Some(PathSegment::CatchAll(s)),
537 PageSegment::OptionalCatchAll(s) => Some(PathSegment::OptionalCatchAll(s)),
538 _ => None,
539 })
540 .collect(),
541 )
542 }
543}
544
545#[cfg(test)]
546mod test {
547 use crate::next_app::{AppPage, PageSegment, PageType};
548
549 #[test]
550 fn test_normalize_metadata_route() {
551 assert_eq!(
552 AppPage::parse("(group)/foo/@par/bar/page.tsx").unwrap(),
553 AppPage(vec![
554 PageSegment::Group("group".into()),
555 PageSegment::Static("foo".into()),
556 PageSegment::Parallel("par".into()),
557 PageSegment::Static("bar".into()),
558 PageSegment::PageType(PageType::Page),
559 ])
560 );
561 assert_eq!(
562 AppPage::parse("(group)/foo/@par/bar/page").unwrap(),
563 AppPage(vec![
564 PageSegment::Group("group".into()),
565 PageSegment::Static("foo".into()),
566 PageSegment::Parallel("par".into()),
567 PageSegment::Static("bar".into()),
568 PageSegment::PageType(PageType::Page),
569 ])
570 );
571
572 assert_eq!(
573 AppPage::parse("(group)/foo/@par/bar/route.tsx").unwrap(),
574 AppPage(vec![
575 PageSegment::Group("group".into()),
576 PageSegment::Static("foo".into()),
577 PageSegment::Parallel("par".into()),
578 PageSegment::Static("bar".into()),
579 PageSegment::PageType(PageType::Route),
580 ])
581 );
582 assert_eq!(
583 AppPage::parse("(group)/foo/@par/bar/route").unwrap(),
584 AppPage(vec![
585 PageSegment::Group("group".into()),
586 PageSegment::Static("foo".into()),
587 PageSegment::Parallel("par".into()),
588 PageSegment::Static("bar".into()),
589 PageSegment::PageType(PageType::Route),
590 ])
591 );
592
593 assert_eq!(
594 AppPage::parse("foo/sitemap").unwrap(),
595 AppPage(vec![
596 PageSegment::Static("foo".into()),
597 PageSegment::Static("sitemap".into()),
598 ])
599 );
600
601 assert_eq!(
602 AppPage::parse("foo/robots.txt").unwrap(),
603 AppPage(vec![
604 PageSegment::Static("foo".into()),
605 PageSegment::Static("robots.txt".into()),
606 ])
607 );
608 }
609}