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