Skip to main content

next_core/next_app/
mod.rs

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/// See [AppPage].
30#[turbo_tasks::task_input]
31#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)]
32pub enum PageSegment {
33    /// e.g. `/dashboard`
34    Static(RcStr),
35    /// e.g. `/[id]`
36    Dynamic(RcStr),
37    /// e.g. `/[...slug]`
38    CatchAll(RcStr),
39    /// e.g. `/[[...slug]]`
40    OptionalCatchAll(RcStr),
41    /// e.g. `/(shop)`
42    Group(RcStr),
43    /// e.g. `/@auth`
44    Parallel(RcStr),
45    /// The final page type appended. (e.g. `/dashboard/page`,
46    /// `/api/hello/route`)
47    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/// Describes the pathname including all internal modifiers such as
140/// intercepting routes, parallel routes and route/page suffixes that are not
141/// part of the pathname.
142#[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            // Next.js internals sometimes omit extensions when creating synthetic page entries
220            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            // can also be metadata (and be neither Page nor Route)
226        }
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    /// The `PageType` is the last segment for completed pages. We need to find
240    /// the last segment that is not a `PageType`, `Group`, or `Parallel`
241    /// segment, because these do not inform the routing structure.
242    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            // The `PageType` is the last segment for completed pages.
261            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    /// Returns true if there is only one segment and it is a group.
276    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        // next.js does some weird stuff when looking up routes, so we have to emit the
311        // correct path (shortest segments, but alphabetically the last).
312        // https://github.com/vercel/next.js/blob/194311d8c96144d68e65cd9abb26924d25978da7/packages/next/src/server/base-server.ts#L3003
313        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/// Path segments for a router path (not including parallel routes and groups).
324///
325/// Also see [AppPath].
326#[turbo_tasks::task_input]
327#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)]
328pub enum PathSegment {
329    /// e.g. `/dashboard`
330    Static(RcStr),
331    /// e.g. `/[id]`
332    Dynamic(RcStr),
333    /// e.g. `/[...slug]`
334    CatchAll(RcStr),
335    /// e.g. `/[[...slug]]`
336    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/// The pathname (including dynamic placeholders) for the next.js router to
363/// resolve.
364///
365/// Does not include internal modifiers as it's the equivalent of the http
366/// request path.
367#[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        // can only be the last segment.
389        matches!(
390            self.last(),
391            Some(PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_))
392        )
393    }
394
395    pub fn contains(&self, other: &AppPath) -> bool {
396        // TODO: handle OptionalCatchAll properly.
397        for (i, segment) in other.0.iter().enumerate() {
398            let Some(self_segment) = self.0.get(i) else {
399                // other is longer than self
400                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    /// Returns true if any segment in the path is an interception route.
421    /// Unlike `AppPage::is_intercepting()`, this also identifies descendants
422    /// below the interception marker.
423    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    /// Returns the ordinary route that an interception route substitutes for.
433    /// This is the route that must handle a direct request or hard refresh.
434    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    /// Returns whether the supplied ordinary route patterns cover every URL matched by this
492    /// route pattern.
493    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        // An optional or required catchall can have its shorter paths covered by fixed routes
519        // before another catchall takes over the remaining suffix.
520        (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        // next.js does some weird stuff when looking up routes, so we have to emit the
618        // correct path (shortest segments, but alphabetically the last).
619        // https://github.com/vercel/next.js/blob/194311d8c96144d68e65cd9abb26924d25978da7/packages/next/src/server/base-server.ts#L3003
620        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}