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::trace::TraceRawVcs;
18
19pub use crate::next_app::{
20 app_client_references_chunks::{
21 ClientReferencesChunks, get_app_client_references_chunks,
22 get_client_references_chunks_for_hmr,
23 },
24 app_client_shared_chunks::get_app_client_shared_chunk_group,
25 app_entry::AppEntry,
26 app_page_entry::get_app_page_entry,
27 app_route_entry::get_app_route_entry,
28};
29
30#[turbo_tasks::task_input]
32#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, TraceRawVcs, Encode, Decode)]
33pub enum PageSegment {
34 Static(RcStr),
36 Dynamic(RcStr),
38 CatchAll(RcStr),
40 OptionalCatchAll(RcStr),
42 Group(RcStr),
44 Parallel(RcStr),
46 PageType(PageType),
49}
50
51impl PageSegment {
52 pub fn parse(segment: &str) -> Result<Self> {
53 if segment.is_empty() {
54 bail!("empty segments are not allowed");
55 }
56
57 if segment.contains('/') {
58 bail!("slashes are not allowed in segments");
59 }
60
61 if let Some(s) = segment.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
62 return Ok(PageSegment::Group(s.into()));
63 }
64
65 if let Some(s) = segment.strip_prefix('@') {
66 return Ok(PageSegment::Parallel(s.into()));
67 }
68
69 if let Some(s) = segment
70 .strip_prefix("[[...")
71 .and_then(|s| s.strip_suffix("]]"))
72 {
73 return Ok(PageSegment::OptionalCatchAll(s.into()));
74 }
75
76 if let Some(s) = segment
77 .strip_prefix("[...")
78 .and_then(|s| s.strip_suffix(']'))
79 {
80 return Ok(PageSegment::CatchAll(s.into()));
81 }
82
83 if let Some(s) = segment.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
84 return Ok(PageSegment::Dynamic(s.into()));
85 }
86
87 Ok(PageSegment::Static(segment.into()))
88 }
89}
90
91impl Display for PageSegment {
92 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
93 match self {
94 PageSegment::Static(s) => f.write_str(s),
95 PageSegment::Dynamic(s) => {
96 f.write_char('[')?;
97 f.write_str(s)?;
98 f.write_char(']')
99 }
100 PageSegment::CatchAll(s) => {
101 f.write_str("[...")?;
102 f.write_str(s)?;
103 f.write_char(']')
104 }
105 PageSegment::OptionalCatchAll(s) => {
106 f.write_str("[[...")?;
107 f.write_str(s)?;
108 f.write_str("]]")
109 }
110 PageSegment::Group(s) => {
111 f.write_char('(')?;
112 f.write_str(s)?;
113 f.write_char(')')
114 }
115 PageSegment::Parallel(s) => {
116 f.write_char('@')?;
117 f.write_str(s)
118 }
119 PageSegment::PageType(s) => Display::fmt(s, f),
120 }
121 }
122}
123
124#[turbo_tasks::task_input]
125#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, TraceRawVcs, Encode, Decode)]
126pub enum PageType {
127 Page,
128 Route,
129}
130
131impl Display for PageType {
132 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
133 f.write_str(match self {
134 PageType::Page => "page",
135 PageType::Route => "route",
136 })
137 }
138}
139
140#[turbo_tasks::task_input]
144#[derive(Clone, Debug, Hash, PartialEq, Eq, Default, TraceRawVcs, Encode, Decode)]
145pub struct AppPage(pub Vec<PageSegment>);
146
147impl AppPage {
148 pub fn new() -> Self {
149 Self::default()
150 }
151
152 pub fn push(&mut self, segment: PageSegment) -> Result<()> {
153 let has_catchall = self.0.iter().any(|segment| {
154 matches!(
155 segment,
156 PageSegment::CatchAll(..) | PageSegment::OptionalCatchAll(..)
157 )
158 });
159
160 if has_catchall
161 && matches!(
162 segment,
163 PageSegment::Static(..)
164 | PageSegment::Dynamic(..)
165 | PageSegment::CatchAll(..)
166 | PageSegment::OptionalCatchAll(..)
167 )
168 {
169 bail!(
170 "Invalid segment {:?}, catch all segment must be the last segment modifying the \
171 path (segments: {:?})",
172 segment,
173 self.0
174 )
175 }
176
177 if self.is_complete() {
178 bail!(
179 "Invalid segment {:?}, this page path already has the final PageType appended \
180 (segments: {:?})",
181 segment,
182 self.0
183 )
184 }
185
186 self.0.push(segment);
187 Ok(())
188 }
189
190 pub fn push_str(&mut self, segment: &str) -> Result<()> {
191 if segment.is_empty() {
192 return Ok(());
193 }
194
195 self.push(PageSegment::parse(segment)?)
196 }
197
198 pub fn clone_push(&self, segment: PageSegment) -> Result<Self> {
199 let mut cloned = self.clone();
200 cloned.push(segment)?;
201 Ok(cloned)
202 }
203
204 pub fn clone_push_str(&self, segment: &str) -> Result<Self> {
205 let mut cloned = self.clone();
206 cloned.push_str(segment)?;
207 Ok(cloned)
208 }
209
210 pub fn parse(page: &str) -> Result<Self> {
211 let mut app_page = Self::new();
212
213 for segment in page.split('/') {
214 app_page.push_str(segment)?;
215 }
216
217 if let Some(last) = app_page.0.last_mut()
218 && let PageSegment::Static(last_name) = &*last
219 {
220 if last_name == "page" || last_name.starts_with("page.") {
222 *last = PageSegment::PageType(PageType::Page);
223 } else if last_name == "route" || last_name.starts_with("route.") {
224 *last = PageSegment::PageType(PageType::Route);
225 }
226 }
228
229 Ok(app_page)
230 }
231
232 pub fn is_root(&self) -> bool {
233 self.0.is_empty()
234 }
235
236 pub fn is_complete(&self) -> bool {
237 matches!(self.0.last(), Some(PageSegment::PageType(..)))
238 }
239
240 pub fn get_last_routing_segment(&self) -> Option<&PageSegment> {
244 self.0.iter().rev().find(|segment| {
245 !matches!(
246 segment,
247 PageSegment::PageType(_) | PageSegment::Group(_) | PageSegment::Parallel(_)
248 )
249 })
250 }
251
252 pub fn is_catchall(&self) -> bool {
253 matches!(
254 self.get_last_routing_segment(),
255 Some(PageSegment::CatchAll(_) | PageSegment::OptionalCatchAll(_))
256 )
257 }
258
259 pub fn is_intercepting(&self) -> bool {
260 let segment = if self.is_complete() {
261 self.0.iter().nth_back(1)
263 } else {
264 self.0.last()
265 };
266
267 matches!(
268 segment,
269 Some(PageSegment::Static(segment))
270 if segment.starts_with("(.)")
271 || segment.starts_with("(..)")
272 || segment.starts_with("(...)")
273 )
274 }
275
276 pub fn is_first_layer_group_route(&self) -> bool {
278 self.0.len() == 1 && matches!(self.0.last(), Some(PageSegment::Group(_)))
279 }
280
281 pub fn complete(&self, page_type: PageType) -> Result<Self> {
282 self.clone_push(PageSegment::PageType(page_type))
283 }
284}
285
286impl Display for AppPage {
287 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
288 if self.0.is_empty() {
289 return f.write_char('/');
290 }
291
292 for segment in &self.0 {
293 f.write_char('/')?;
294 Display::fmt(segment, f)?;
295 }
296
297 Ok(())
298 }
299}
300
301impl Deref for AppPage {
302 type Target = [PageSegment];
303
304 fn deref(&self) -> &Self::Target {
305 &self.0
306 }
307}
308
309impl Ord for AppPage {
310 fn cmp(&self, other: &Self) -> Ordering {
311 self.len().cmp(&other.len()).then(other.0.cmp(&self.0))
315 }
316}
317
318impl PartialOrd for AppPage {
319 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
320 Some(self.cmp(other))
321 }
322}
323
324#[turbo_tasks::task_input]
328#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, TraceRawVcs, Encode, Decode)]
329pub enum PathSegment {
330 Static(RcStr),
332 Dynamic(RcStr),
334 CatchAll(RcStr),
336 OptionalCatchAll(RcStr),
338}
339
340impl Display for PathSegment {
341 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
342 match self {
343 PathSegment::Static(s) => f.write_str(s),
344 PathSegment::Dynamic(s) => {
345 f.write_char('[')?;
346 f.write_str(s)?;
347 f.write_char(']')
348 }
349 PathSegment::CatchAll(s) => {
350 f.write_str("[...")?;
351 f.write_str(s)?;
352 f.write_char(']')
353 }
354 PathSegment::OptionalCatchAll(s) => {
355 f.write_str("[[...")?;
356 f.write_str(s)?;
357 f.write_str("]]")
358 }
359 }
360 }
361}
362
363#[turbo_tasks::task_input]
369#[derive(Clone, Debug, Hash, PartialEq, Eq, Default, TraceRawVcs, Encode, Decode)]
370pub struct AppPath(pub Vec<PathSegment>);
371
372impl AppPath {
373 pub fn is_dynamic(&self) -> bool {
374 self.iter().any(|segment| {
375 matches!(
376 (segment,),
377 (PathSegment::Dynamic(_)
378 | PathSegment::CatchAll(_)
379 | PathSegment::OptionalCatchAll(_),)
380 )
381 })
382 }
383
384 pub fn is_root(&self) -> bool {
385 self.0.is_empty()
386 }
387
388 pub fn is_catchall(&self) -> bool {
389 matches!(
391 self.last(),
392 Some(PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_))
393 )
394 }
395
396 pub fn contains(&self, other: &AppPath) -> bool {
397 for (i, segment) in other.0.iter().enumerate() {
399 let Some(self_segment) = self.0.get(i) else {
400 return false;
402 };
403
404 if self_segment == segment {
405 continue;
406 }
407
408 if matches!(
409 segment,
410 PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_)
411 ) {
412 return true;
413 }
414
415 return false;
416 }
417
418 true
419 }
420
421 pub fn contains_interception(&self) -> bool {
425 self.iter().any(|segment| {
426 matches!(
427 segment,
428 PathSegment::Static(s) if s.starts_with("(.)") || s.starts_with("(..)") || s.starts_with("(...)")
429 )
430 })
431 }
432
433 pub fn intercepted_path(&self) -> Option<AppPath> {
436 let (interception_index, segment) =
437 self.iter().enumerate().find_map(|(index, segment)| {
438 let PathSegment::Static(segment) = segment else {
439 return None;
440 };
441
442 let (marker, target) =
443 ["(..)(..)", "(...)", "(..)", "(.)"]
444 .into_iter()
445 .find_map(|marker| {
446 segment.strip_prefix(marker).map(|target| (marker, target))
447 })?;
448 Some((index, (marker, target)))
449 })?;
450
451 let (marker, target) = segment;
452 if target.is_empty() {
453 return None;
454 }
455
456 let mut canonical_segments = match marker {
457 "(...)" => Vec::new(),
458 _ => self.0[..interception_index].to_vec(),
459 };
460 let levels_to_pop = match marker {
461 "(..)(..)" => 2,
462 "(..)" => 1,
463 _ => 0,
464 };
465 for _ in 0..levels_to_pop {
466 canonical_segments.pop()?;
467 }
468 let target = if let Some(target) = target
469 .strip_prefix("[[...")
470 .and_then(|target| target.strip_suffix("]]"))
471 {
472 PathSegment::OptionalCatchAll(target.into())
473 } else if let Some(target) = target
474 .strip_prefix("[...")
475 .and_then(|target| target.strip_suffix(']'))
476 {
477 PathSegment::CatchAll(target.into())
478 } else if let Some(target) = target
479 .strip_prefix('[')
480 .and_then(|target| target.strip_suffix(']'))
481 {
482 PathSegment::Dynamic(target.into())
483 } else {
484 PathSegment::Static(target.into())
485 };
486 canonical_segments.push(target);
487 canonical_segments.extend_from_slice(&self.0[interception_index + 1..]);
488
489 Some(AppPath(canonical_segments))
490 }
491
492 pub fn is_route_pattern_covered_by<'a>(
495 &self,
496 ordinary_routes: impl IntoIterator<Item = &'a AppPath>,
497 ) -> bool {
498 let route = RoutePattern::new(self);
499 let ordinary_routes = ordinary_routes
500 .into_iter()
501 .map(RoutePattern::new)
502 .collect::<Vec<_>>();
503
504 if !route.unbounded {
505 return ordinary_routes
506 .iter()
507 .any(|ordinary| pattern_covers_at_length(ordinary, &route, route.min_length));
508 }
509
510 let Some(unbounded_coverage_start) = ordinary_routes
511 .iter()
512 .filter(|ordinary| ordinary.unbounded && prefix_covers(ordinary, &route))
513 .map(|ordinary| ordinary.min_length.max(route.min_length))
514 .min()
515 else {
516 return false;
517 };
518
519 (route.min_length..unbounded_coverage_start).all(|length| {
522 ordinary_routes
523 .iter()
524 .any(|ordinary| pattern_covers_at_length(ordinary, &route, length))
525 })
526 }
527}
528
529struct RoutePattern<'a> {
530 prefix: &'a [PathSegment],
531 min_length: usize,
532 unbounded: bool,
533}
534
535impl<'a> RoutePattern<'a> {
536 fn new(path: &'a AppPath) -> Self {
537 match path.last() {
538 Some(PathSegment::CatchAll(_)) => Self {
539 prefix: &path[..path.len() - 1],
540 min_length: path.len(),
541 unbounded: true,
542 },
543 Some(PathSegment::OptionalCatchAll(_)) => Self {
544 prefix: &path[..path.len() - 1],
545 min_length: path.len() - 1,
546 unbounded: true,
547 },
548 _ => Self {
549 prefix: path,
550 min_length: path.len(),
551 unbounded: false,
552 },
553 }
554 }
555}
556
557fn accepts_length(pattern: &RoutePattern<'_>, length: usize) -> bool {
558 if pattern.unbounded {
559 length >= pattern.min_length
560 } else {
561 length == pattern.min_length
562 }
563}
564
565fn prefix_covers(canonical: &RoutePattern<'_>, intercepted: &RoutePattern<'_>) -> bool {
566 canonical
567 .prefix
568 .iter()
569 .enumerate()
570 .all(|(index, canonical_segment)| match canonical_segment {
571 PathSegment::Dynamic(_) => true,
572 PathSegment::Static(canonical_segment) => matches!(
573 intercepted.prefix.get(index),
574 Some(PathSegment::Static(intercepted_segment))
575 if canonical_segment == intercepted_segment
576 ),
577 PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_) => {
578 unreachable!("catchall segments are excluded from the route prefix")
579 }
580 })
581}
582
583fn pattern_covers_at_length(
584 canonical: &RoutePattern<'_>,
585 intercepted: &RoutePattern<'_>,
586 length: usize,
587) -> bool {
588 accepts_length(canonical, length)
589 && accepts_length(intercepted, length)
590 && prefix_covers(canonical, intercepted)
591}
592
593impl Deref for AppPath {
594 type Target = [PathSegment];
595
596 fn deref(&self) -> &Self::Target {
597 &self.0
598 }
599}
600
601impl Display for AppPath {
602 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
603 if self.0.is_empty() {
604 return f.write_char('/');
605 }
606
607 for segment in &self.0 {
608 f.write_char('/')?;
609 Display::fmt(segment, f)?;
610 }
611
612 Ok(())
613 }
614}
615
616impl Ord for AppPath {
617 fn cmp(&self, other: &Self) -> Ordering {
618 self.len().cmp(&other.len()).then(other.0.cmp(&self.0))
622 }
623}
624
625impl PartialOrd for AppPath {
626 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
627 Some(self.cmp(other))
628 }
629}
630
631impl From<AppPage> for AppPath {
632 fn from(value: AppPage) -> Self {
633 AppPath(
634 value
635 .0
636 .into_iter()
637 .filter_map(|segment| match segment {
638 PageSegment::Static(s) => Some(PathSegment::Static(s)),
639 PageSegment::Dynamic(s) => Some(PathSegment::Dynamic(s)),
640 PageSegment::CatchAll(s) => Some(PathSegment::CatchAll(s)),
641 PageSegment::OptionalCatchAll(s) => Some(PathSegment::OptionalCatchAll(s)),
642 _ => None,
643 })
644 .collect(),
645 )
646 }
647}
648
649#[cfg(test)]
650mod test {
651 use crate::next_app::{AppPage, AppPath, PageSegment, PageType};
652
653 #[test]
654 fn resolves_intercepted_app_paths() {
655 for (interception, canonical) in [
656 ("/(.)photo/[id]", "/photo/[id]"),
657 ("/feed/(.)photo/[id]", "/feed/photo/[id]"),
658 ("/feed/(..)photo/[id]", "/photo/[id]"),
659 ("/feed/nested/(..)(..)photo/[id]", "/photo/[id]"),
660 ("/feed/(...)photo/[id]", "/photo/[id]"),
661 ("/(.)[username]/[id]", "/[username]/[id]"),
662 ("/(.)[...slug]", "/[...slug]"),
663 ("/(.)[[...slug]]", "/[[...slug]]"),
664 ] {
665 let interception =
666 AppPath::from(AppPage::parse(interception.trim_start_matches('/')).unwrap());
667 let canonical =
668 AppPath::from(AppPage::parse(canonical.trim_start_matches('/')).unwrap());
669
670 assert_eq!(interception.intercepted_path(), Some(canonical));
671 }
672 }
673
674 #[test]
675 fn checks_route_pattern_coverage() {
676 for (route, ordinary_routes, expected) in [
677 ("/photo/[id]", &["/photo/[slug]"][..], true),
678 ("/showcase/[...parts]", &["/[...slug]"][..], true),
679 (
680 "/items/[...parts]",
681 &["/items/[id]", "/items/[id]/[...rest]"][..],
682 true,
683 ),
684 (
685 "/items/[[...parts]]",
686 &["/items", "/items/[...rest]"][..],
687 true,
688 ),
689 ("/photo/[id]", &[][..], false),
690 ("/items/[...parts]", &["/items/[id]"][..], false),
691 ("/items/[[...parts]]", &["/items/[...rest]"][..], false),
692 ("/items/[id]", &["/items/one", "/items/two"][..], false),
693 ] {
694 let route = AppPath::from(AppPage::parse(route.trim_start_matches('/')).unwrap());
695 let ordinary_routes = ordinary_routes
696 .iter()
697 .map(|route| AppPath::from(AppPage::parse(route.trim_start_matches('/')).unwrap()))
698 .collect::<Vec<_>>();
699
700 assert_eq!(
701 route.is_route_pattern_covered_by(ordinary_routes.iter()),
702 expected,
703 "coverage for {route} from {ordinary_routes:?}"
704 );
705 }
706 }
707
708 #[test]
709 fn test_normalize_metadata_route() {
710 assert_eq!(
711 AppPage::parse("(group)/foo/@par/bar/page.tsx").unwrap(),
712 AppPage(vec![
713 PageSegment::Group("group".into()),
714 PageSegment::Static("foo".into()),
715 PageSegment::Parallel("par".into()),
716 PageSegment::Static("bar".into()),
717 PageSegment::PageType(PageType::Page),
718 ])
719 );
720 assert_eq!(
721 AppPage::parse("(group)/foo/@par/bar/page").unwrap(),
722 AppPage(vec![
723 PageSegment::Group("group".into()),
724 PageSegment::Static("foo".into()),
725 PageSegment::Parallel("par".into()),
726 PageSegment::Static("bar".into()),
727 PageSegment::PageType(PageType::Page),
728 ])
729 );
730
731 assert_eq!(
732 AppPage::parse("(group)/foo/@par/bar/route.tsx").unwrap(),
733 AppPage(vec![
734 PageSegment::Group("group".into()),
735 PageSegment::Static("foo".into()),
736 PageSegment::Parallel("par".into()),
737 PageSegment::Static("bar".into()),
738 PageSegment::PageType(PageType::Route),
739 ])
740 );
741 assert_eq!(
742 AppPage::parse("(group)/foo/@par/bar/route").unwrap(),
743 AppPage(vec![
744 PageSegment::Group("group".into()),
745 PageSegment::Static("foo".into()),
746 PageSegment::Parallel("par".into()),
747 PageSegment::Static("bar".into()),
748 PageSegment::PageType(PageType::Route),
749 ])
750 );
751
752 assert_eq!(
753 AppPage::parse("foo/sitemap").unwrap(),
754 AppPage(vec![
755 PageSegment::Static("foo".into()),
756 PageSegment::Static("sitemap".into()),
757 ])
758 );
759
760 assert_eq!(
761 AppPage::parse("foo/robots.txt").unwrap(),
762 AppPage(vec![
763 PageSegment::Static("foo".into()),
764 PageSegment::Static("robots.txt".into()),
765 ])
766 );
767 }
768}