next.js/crates/next-core/src/next_app/mod.rs
mod.rs610 lines16.2 KB
pub mod app_client_references_chunks;
pub mod app_client_shared_chunks;
pub mod app_entry;
pub mod app_page_entry;
pub mod app_route_entry;
pub mod metadata;

use std::{
    cmp::Ordering,
    fmt::{Display, Formatter, Write},
    ops::Deref,
};

use anyhow::{Result, bail};
use bincode::{Decode, Encode};
use turbo_rcstr::RcStr;
use turbo_tasks::{NonLocalValue, TaskInput, trace::TraceRawVcs};

pub use crate::next_app::{
    app_client_references_chunks::{ClientReferencesChunks, get_app_client_references_chunks},
    app_client_shared_chunks::get_app_client_shared_chunk_group,
    app_entry::AppEntry,
    app_page_entry::get_app_page_entry,
    app_route_entry::get_app_route_entry,
};

/// See [AppPage].
#[derive(
    Clone,
    Debug,
    Hash,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    TaskInput,
    TraceRawVcs,
    NonLocalValue,
    Encode,
    Decode,
)]
pub enum PageSegment {
    /// e.g. `/dashboard`
    Static(RcStr),
    /// e.g. `/[id]`
    Dynamic(RcStr),
    /// e.g. `/[...slug]`
    CatchAll(RcStr),
    /// e.g. `/[[...slug]]`
    OptionalCatchAll(RcStr),
    /// e.g. `/(shop)`
    Group(RcStr),
    /// e.g. `/@auth`
    Parallel(RcStr),
    /// The final page type appended. (e.g. `/dashboard/page`,
    /// `/api/hello/route`)
    PageType(PageType),
}

impl PageSegment {
    pub fn parse(segment: &str) -> Result<Self> {
        if segment.is_empty() {
            bail!("empty segments are not allowed");
        }

        if segment.contains('/') {
            bail!("slashes are not allowed in segments");
        }

        if let Some(s) = segment.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
            return Ok(PageSegment::Group(s.into()));
        }

        if let Some(s) = segment.strip_prefix('@') {
            return Ok(PageSegment::Parallel(s.into()));
        }

        if let Some(s) = segment
            .strip_prefix("[[...")
            .and_then(|s| s.strip_suffix("]]"))
        {
            return Ok(PageSegment::OptionalCatchAll(s.into()));
        }

        if let Some(s) = segment
            .strip_prefix("[...")
            .and_then(|s| s.strip_suffix(']'))
        {
            return Ok(PageSegment::CatchAll(s.into()));
        }

        if let Some(s) = segment.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
            return Ok(PageSegment::Dynamic(s.into()));
        }

        Ok(PageSegment::Static(segment.into()))
    }
}

impl Display for PageSegment {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            PageSegment::Static(s) => f.write_str(s),
            PageSegment::Dynamic(s) => {
                f.write_char('[')?;
                f.write_str(s)?;
                f.write_char(']')
            }
            PageSegment::CatchAll(s) => {
                f.write_str("[...")?;
                f.write_str(s)?;
                f.write_char(']')
            }
            PageSegment::OptionalCatchAll(s) => {
                f.write_str("[[...")?;
                f.write_str(s)?;
                f.write_str("]]")
            }
            PageSegment::Group(s) => {
                f.write_char('(')?;
                f.write_str(s)?;
                f.write_char(')')
            }
            PageSegment::Parallel(s) => {
                f.write_char('@')?;
                f.write_str(s)
            }
            PageSegment::PageType(s) => Display::fmt(s, f),
        }
    }
}

#[derive(
    Clone,
    Debug,
    Hash,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    TaskInput,
    TraceRawVcs,
    NonLocalValue,
    Encode,
    Decode,
)]
pub enum PageType {
    Page,
    Route,
}

impl Display for PageType {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            PageType::Page => "page",
            PageType::Route => "route",
        })
    }
}

/// Describes the pathname including all internal modifiers such as
/// intercepting routes, parallel routes and route/page suffixes that are not
/// part of the pathname.
#[derive(
    Clone,
    Debug,
    Hash,
    PartialEq,
    Eq,
    Default,
    TaskInput,
    TraceRawVcs,
    NonLocalValue,
    Encode,
    Decode,
)]
pub struct AppPage(pub Vec<PageSegment>);

impl AppPage {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn push(&mut self, segment: PageSegment) -> Result<()> {
        let has_catchall = self.0.iter().any(|segment| {
            matches!(
                segment,
                PageSegment::CatchAll(..) | PageSegment::OptionalCatchAll(..)
            )
        });

        if has_catchall
            && matches!(
                segment,
                PageSegment::Static(..)
                    | PageSegment::Dynamic(..)
                    | PageSegment::CatchAll(..)
                    | PageSegment::OptionalCatchAll(..)
            )
        {
            bail!(
                "Invalid segment {:?}, catch all segment must be the last segment modifying the \
                 path (segments: {:?})",
                segment,
                self.0
            )
        }

        if self.is_complete() {
            bail!(
                "Invalid segment {:?}, this page path already has the final PageType appended \
                 (segments: {:?})",
                segment,
                self.0
            )
        }

        self.0.push(segment);
        Ok(())
    }

    pub fn push_str(&mut self, segment: &str) -> Result<()> {
        if segment.is_empty() {
            return Ok(());
        }

        self.push(PageSegment::parse(segment)?)
    }

    pub fn clone_push(&self, segment: PageSegment) -> Result<Self> {
        let mut cloned = self.clone();
        cloned.push(segment)?;
        Ok(cloned)
    }

    pub fn clone_push_str(&self, segment: &str) -> Result<Self> {
        let mut cloned = self.clone();
        cloned.push_str(segment)?;
        Ok(cloned)
    }

    pub fn parse(page: &str) -> Result<Self> {
        let mut app_page = Self::new();

        for segment in page.split('/') {
            app_page.push_str(segment)?;
        }

        if let Some(last) = app_page.0.last_mut()
            && let PageSegment::Static(last_name) = &*last
        {
            // Next.js internals sometimes omit extensions when creating synthetic page entries
            if last_name == "page" || last_name.starts_with("page.") {
                *last = PageSegment::PageType(PageType::Page);
            } else if last_name == "route" || last_name.starts_with("route.") {
                *last = PageSegment::PageType(PageType::Route);
            }
            // can also be metadata (and be neither Page nor Route)
        }

        Ok(app_page)
    }

    pub fn is_root(&self) -> bool {
        self.0.is_empty()
    }

    pub fn is_complete(&self) -> bool {
        matches!(self.0.last(), Some(PageSegment::PageType(..)))
    }

    /// The `PageType` is the last segment for completed pages. We need to find
    /// the last segment that is not a `PageType`, `Group`, or `Parallel`
    /// segment, because these do not inform the routing structure.
    pub fn get_last_routing_segment(&self) -> Option<&PageSegment> {
        self.0.iter().rev().find(|segment| {
            !matches!(
                segment,
                PageSegment::PageType(_) | PageSegment::Group(_) | PageSegment::Parallel(_)
            )
        })
    }

    pub fn is_catchall(&self) -> bool {
        matches!(
            self.get_last_routing_segment(),
            Some(PageSegment::CatchAll(_) | PageSegment::OptionalCatchAll(_))
        )
    }

    pub fn is_intercepting(&self) -> bool {
        let segment = if self.is_complete() {
            // The `PageType` is the last segment for completed pages.
            self.0.iter().nth_back(1)
        } else {
            self.0.last()
        };

        matches!(
            segment,
            Some(PageSegment::Static(segment))
                if segment.starts_with("(.)")
                    || segment.starts_with("(..)")
                    || segment.starts_with("(...)")
        )
    }

    /// Returns true if there is only one segment and it is a group.
    pub fn is_first_layer_group_route(&self) -> bool {
        self.0.len() == 1 && matches!(self.0.last(), Some(PageSegment::Group(_)))
    }

    pub fn complete(&self, page_type: PageType) -> Result<Self> {
        self.clone_push(PageSegment::PageType(page_type))
    }
}

impl Display for AppPage {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        if self.0.is_empty() {
            return f.write_char('/');
        }

        for segment in &self.0 {
            f.write_char('/')?;
            Display::fmt(segment, f)?;
        }

        Ok(())
    }
}

impl Deref for AppPage {
    type Target = [PageSegment];

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl Ord for AppPage {
    fn cmp(&self, other: &Self) -> Ordering {
        // next.js does some weird stuff when looking up routes, so we have to emit the
        // correct path (shortest segments, but alphabetically the last).
        // https://github.com/vercel/next.js/blob/194311d8c96144d68e65cd9abb26924d25978da7/packages/next/src/server/base-server.ts#L3003
        self.len().cmp(&other.len()).then(other.0.cmp(&self.0))
    }
}

impl PartialOrd for AppPage {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

/// Path segments for a router path (not including parallel routes and groups).
///
/// Also see [AppPath].
#[derive(
    Clone,
    Debug,
    Hash,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    TaskInput,
    TraceRawVcs,
    NonLocalValue,
    Encode,
    Decode,
)]
pub enum PathSegment {
    /// e.g. `/dashboard`
    Static(RcStr),
    /// e.g. `/[id]`
    Dynamic(RcStr),
    /// e.g. `/[...slug]`
    CatchAll(RcStr),
    /// e.g. `/[[...slug]]`
    OptionalCatchAll(RcStr),
}

impl Display for PathSegment {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            PathSegment::Static(s) => f.write_str(s),
            PathSegment::Dynamic(s) => {
                f.write_char('[')?;
                f.write_str(s)?;
                f.write_char(']')
            }
            PathSegment::CatchAll(s) => {
                f.write_str("[...")?;
                f.write_str(s)?;
                f.write_char(']')
            }
            PathSegment::OptionalCatchAll(s) => {
                f.write_str("[[...")?;
                f.write_str(s)?;
                f.write_str("]]")
            }
        }
    }
}

/// The pathname (including dynamic placeholders) for the next.js router to
/// resolve.
///
/// Does not include internal modifiers as it's the equivalent of the http
/// request path.
#[derive(
    Clone,
    Debug,
    Hash,
    PartialEq,
    Eq,
    Default,
    TaskInput,
    TraceRawVcs,
    NonLocalValue,
    Encode,
    Decode,
)]
pub struct AppPath(pub Vec<PathSegment>);

impl AppPath {
    pub fn is_dynamic(&self) -> bool {
        self.iter().any(|segment| {
            matches!(
                (segment,),
                (PathSegment::Dynamic(_)
                    | PathSegment::CatchAll(_)
                    | PathSegment::OptionalCatchAll(_),)
            )
        })
    }

    pub fn is_root(&self) -> bool {
        self.0.is_empty()
    }

    pub fn is_catchall(&self) -> bool {
        // can only be the last segment.
        matches!(
            self.last(),
            Some(PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_))
        )
    }

    pub fn contains(&self, other: &AppPath) -> bool {
        // TODO: handle OptionalCatchAll properly.
        for (i, segment) in other.0.iter().enumerate() {
            let Some(self_segment) = self.0.get(i) else {
                // other is longer than self
                return false;
            };

            if self_segment == segment {
                continue;
            }

            if matches!(
                segment,
                PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_)
            ) {
                return true;
            }

            return false;
        }

        true
    }

    /// Returns true if ANY segment in the entire path is an interception route.
    /// This is different from `is_intercepting()` which only checks the last
    /// segment.
    pub fn contains_interception(&self) -> bool {
        self.iter().any(|segment| {
            matches!(
                segment,
                PathSegment::Static(s) if s.starts_with("(.)") || s.starts_with("(..)") || s.starts_with("(...)")
            )
        })
    }
}

impl Deref for AppPath {
    type Target = [PathSegment];

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl Display for AppPath {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        if self.0.is_empty() {
            return f.write_char('/');
        }

        for segment in &self.0 {
            f.write_char('/')?;
            Display::fmt(segment, f)?;
        }

        Ok(())
    }
}

impl Ord for AppPath {
    fn cmp(&self, other: &Self) -> Ordering {
        // next.js does some weird stuff when looking up routes, so we have to emit the
        // correct path (shortest segments, but alphabetically the last).
        // https://github.com/vercel/next.js/blob/194311d8c96144d68e65cd9abb26924d25978da7/packages/next/src/server/base-server.ts#L3003
        self.len().cmp(&other.len()).then(other.0.cmp(&self.0))
    }
}

impl PartialOrd for AppPath {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl From<AppPage> for AppPath {
    fn from(value: AppPage) -> Self {
        AppPath(
            value
                .0
                .into_iter()
                .filter_map(|segment| match segment {
                    PageSegment::Static(s) => Some(PathSegment::Static(s)),
                    PageSegment::Dynamic(s) => Some(PathSegment::Dynamic(s)),
                    PageSegment::CatchAll(s) => Some(PathSegment::CatchAll(s)),
                    PageSegment::OptionalCatchAll(s) => Some(PathSegment::OptionalCatchAll(s)),
                    _ => None,
                })
                .collect(),
        )
    }
}

#[cfg(test)]
mod test {
    use crate::next_app::{AppPage, PageSegment, PageType};

    #[test]
    fn test_normalize_metadata_route() {
        assert_eq!(
            AppPage::parse("(group)/foo/@par/bar/page.tsx").unwrap(),
            AppPage(vec![
                PageSegment::Group("group".into()),
                PageSegment::Static("foo".into()),
                PageSegment::Parallel("par".into()),
                PageSegment::Static("bar".into()),
                PageSegment::PageType(PageType::Page),
            ])
        );
        assert_eq!(
            AppPage::parse("(group)/foo/@par/bar/page").unwrap(),
            AppPage(vec![
                PageSegment::Group("group".into()),
                PageSegment::Static("foo".into()),
                PageSegment::Parallel("par".into()),
                PageSegment::Static("bar".into()),
                PageSegment::PageType(PageType::Page),
            ])
        );

        assert_eq!(
            AppPage::parse("(group)/foo/@par/bar/route.tsx").unwrap(),
            AppPage(vec![
                PageSegment::Group("group".into()),
                PageSegment::Static("foo".into()),
                PageSegment::Parallel("par".into()),
                PageSegment::Static("bar".into()),
                PageSegment::PageType(PageType::Route),
            ])
        );
        assert_eq!(
            AppPage::parse("(group)/foo/@par/bar/route").unwrap(),
            AppPage(vec![
                PageSegment::Group("group".into()),
                PageSegment::Static("foo".into()),
                PageSegment::Parallel("par".into()),
                PageSegment::Static("bar".into()),
                PageSegment::PageType(PageType::Route),
            ])
        );

        assert_eq!(
            AppPage::parse("foo/sitemap").unwrap(),
            AppPage(vec![
                PageSegment::Static("foo".into()),
                PageSegment::Static("sitemap".into()),
            ])
        );

        assert_eq!(
            AppPage::parse("foo/robots.txt").unwrap(),
            AppPage(vec![
                PageSegment::Static("foo".into()),
                PageSegment::Static("robots.txt".into()),
            ])
        );
    }
}
Quest for Codev2.0.0
/
SIGN IN