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;
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/// See [AppPage].
31#[turbo_tasks::task_input]
32#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, TraceRawVcs, Encode, Decode)]
33pub enum PageSegment {
34    /// e.g. `/dashboard`
35    Static(RcStr),
36    /// e.g. `/[id]`
37    Dynamic(RcStr),
38    /// e.g. `/[...slug]`
39    CatchAll(RcStr),
40    /// e.g. `/[[...slug]]`
41    OptionalCatchAll(RcStr),
42    /// e.g. `/(shop)`
43    Group(RcStr),
44    /// e.g. `/@auth`
45    Parallel(RcStr),
46    /// The final page type appended. (e.g. `/dashboard/page`,
47    /// `/api/hello/route`)
48    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/// Describes the pathname including all internal modifiers such as
141/// intercepting routes, parallel routes and route/page suffixes that are not
142/// part of the pathname.
143#[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            // Next.js internals sometimes omit extensions when creating synthetic page entries
221            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            // can also be metadata (and be neither Page nor Route)
227        }
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    /// The `PageType` is the last segment for completed pages. We need to find
241    /// the last segment that is not a `PageType`, `Group`, or `Parallel`
242    /// segment, because these do not inform the routing structure.
243    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            // The `PageType` is the last segment for completed pages.
262            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    /// Returns true if there is only one segment and it is a group.
277    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        // next.js does some weird stuff when looking up routes, so we have to emit the
312        // correct path (shortest segments, but alphabetically the last).
313        // https://github.com/vercel/next.js/blob/194311d8c96144d68e65cd9abb26924d25978da7/packages/next/src/server/base-server.ts#L3003
314        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/// Path segments for a router path (not including parallel routes and groups).
325///
326/// Also see [AppPath].
327#[turbo_tasks::task_input]
328#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, TraceRawVcs, Encode, Decode)]
329pub enum PathSegment {
330    /// e.g. `/dashboard`
331    Static(RcStr),
332    /// e.g. `/[id]`
333    Dynamic(RcStr),
334    /// e.g. `/[...slug]`
335    CatchAll(RcStr),
336    /// e.g. `/[[...slug]]`
337    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/// The pathname (including dynamic placeholders) for the next.js router to
364/// resolve.
365///
366/// Does not include internal modifiers as it's the equivalent of the http
367/// request path.
368#[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        // can only be the last segment.
390        matches!(
391            self.last(),
392            Some(PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_))
393        )
394    }
395
396    pub fn contains(&self, other: &AppPath) -> bool {
397        // TODO: handle OptionalCatchAll properly.
398        for (i, segment) in other.0.iter().enumerate() {
399            let Some(self_segment) = self.0.get(i) else {
400                // other is longer than self
401                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    /// Returns true if ANY segment in the entire path is an interception route.
422    /// This is different from `is_intercepting()` which only checks the last
423    /// segment.
424    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
434impl Deref for AppPath {
435    type Target = [PathSegment];
436
437    fn deref(&self) -> &Self::Target {
438        &self.0
439    }
440}
441
442impl Display for AppPath {
443    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
444        if self.0.is_empty() {
445            return f.write_char('/');
446        }
447
448        for segment in &self.0 {
449            f.write_char('/')?;
450            Display::fmt(segment, f)?;
451        }
452
453        Ok(())
454    }
455}
456
457impl Ord for AppPath {
458    fn cmp(&self, other: &Self) -> Ordering {
459        // next.js does some weird stuff when looking up routes, so we have to emit the
460        // correct path (shortest segments, but alphabetically the last).
461        // https://github.com/vercel/next.js/blob/194311d8c96144d68e65cd9abb26924d25978da7/packages/next/src/server/base-server.ts#L3003
462        self.len().cmp(&other.len()).then(other.0.cmp(&self.0))
463    }
464}
465
466impl PartialOrd for AppPath {
467    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
468        Some(self.cmp(other))
469    }
470}
471
472impl From<AppPage> for AppPath {
473    fn from(value: AppPage) -> Self {
474        AppPath(
475            value
476                .0
477                .into_iter()
478                .filter_map(|segment| match segment {
479                    PageSegment::Static(s) => Some(PathSegment::Static(s)),
480                    PageSegment::Dynamic(s) => Some(PathSegment::Dynamic(s)),
481                    PageSegment::CatchAll(s) => Some(PathSegment::CatchAll(s)),
482                    PageSegment::OptionalCatchAll(s) => Some(PathSegment::OptionalCatchAll(s)),
483                    _ => None,
484                })
485                .collect(),
486        )
487    }
488}
489
490#[cfg(test)]
491mod test {
492    use crate::next_app::{AppPage, PageSegment, PageType};
493
494    #[test]
495    fn test_normalize_metadata_route() {
496        assert_eq!(
497            AppPage::parse("(group)/foo/@par/bar/page.tsx").unwrap(),
498            AppPage(vec![
499                PageSegment::Group("group".into()),
500                PageSegment::Static("foo".into()),
501                PageSegment::Parallel("par".into()),
502                PageSegment::Static("bar".into()),
503                PageSegment::PageType(PageType::Page),
504            ])
505        );
506        assert_eq!(
507            AppPage::parse("(group)/foo/@par/bar/page").unwrap(),
508            AppPage(vec![
509                PageSegment::Group("group".into()),
510                PageSegment::Static("foo".into()),
511                PageSegment::Parallel("par".into()),
512                PageSegment::Static("bar".into()),
513                PageSegment::PageType(PageType::Page),
514            ])
515        );
516
517        assert_eq!(
518            AppPage::parse("(group)/foo/@par/bar/route.tsx").unwrap(),
519            AppPage(vec![
520                PageSegment::Group("group".into()),
521                PageSegment::Static("foo".into()),
522                PageSegment::Parallel("par".into()),
523                PageSegment::Static("bar".into()),
524                PageSegment::PageType(PageType::Route),
525            ])
526        );
527        assert_eq!(
528            AppPage::parse("(group)/foo/@par/bar/route").unwrap(),
529            AppPage(vec![
530                PageSegment::Group("group".into()),
531                PageSegment::Static("foo".into()),
532                PageSegment::Parallel("par".into()),
533                PageSegment::Static("bar".into()),
534                PageSegment::PageType(PageType::Route),
535            ])
536        );
537
538        assert_eq!(
539            AppPage::parse("foo/sitemap").unwrap(),
540            AppPage(vec![
541                PageSegment::Static("foo".into()),
542                PageSegment::Static("sitemap".into()),
543            ])
544        );
545
546        assert_eq!(
547            AppPage::parse("foo/robots.txt").unwrap(),
548            AppPage(vec![
549                PageSegment::Static("foo".into()),
550                PageSegment::Static("robots.txt".into()),
551            ])
552        );
553    }
554}