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::{NonLocalValue, TaskInput, trace::TraceRawVcs};
18
19pub use crate::next_app::{
20    app_client_references_chunks::{ClientReferencesChunks, get_app_client_references_chunks},
21    app_client_shared_chunks::get_app_client_shared_chunk_group,
22    app_entry::AppEntry,
23    app_page_entry::get_app_page_entry,
24    app_route_entry::get_app_route_entry,
25};
26
27/// See [AppPage].
28#[derive(
29    Clone,
30    Debug,
31    Hash,
32    PartialEq,
33    Eq,
34    PartialOrd,
35    Ord,
36    TaskInput,
37    TraceRawVcs,
38    NonLocalValue,
39    Encode,
40    Decode,
41)]
42pub enum PageSegment {
43    /// e.g. `/dashboard`
44    Static(RcStr),
45    /// e.g. `/[id]`
46    Dynamic(RcStr),
47    /// e.g. `/[...slug]`
48    CatchAll(RcStr),
49    /// e.g. `/[[...slug]]`
50    OptionalCatchAll(RcStr),
51    /// e.g. `/(shop)`
52    Group(RcStr),
53    /// e.g. `/@auth`
54    Parallel(RcStr),
55    /// The final page type appended. (e.g. `/dashboard/page`,
56    /// `/api/hello/route`)
57    PageType(PageType),
58}
59
60impl PageSegment {
61    pub fn parse(segment: &str) -> Result<Self> {
62        if segment.is_empty() {
63            bail!("empty segments are not allowed");
64        }
65
66        if segment.contains('/') {
67            bail!("slashes are not allowed in segments");
68        }
69
70        if let Some(s) = segment.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
71            return Ok(PageSegment::Group(s.into()));
72        }
73
74        if let Some(s) = segment.strip_prefix('@') {
75            return Ok(PageSegment::Parallel(s.into()));
76        }
77
78        if let Some(s) = segment
79            .strip_prefix("[[...")
80            .and_then(|s| s.strip_suffix("]]"))
81        {
82            return Ok(PageSegment::OptionalCatchAll(s.into()));
83        }
84
85        if let Some(s) = segment
86            .strip_prefix("[...")
87            .and_then(|s| s.strip_suffix(']'))
88        {
89            return Ok(PageSegment::CatchAll(s.into()));
90        }
91
92        if let Some(s) = segment.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
93            return Ok(PageSegment::Dynamic(s.into()));
94        }
95
96        Ok(PageSegment::Static(segment.into()))
97    }
98}
99
100impl Display for PageSegment {
101    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
102        match self {
103            PageSegment::Static(s) => f.write_str(s),
104            PageSegment::Dynamic(s) => {
105                f.write_char('[')?;
106                f.write_str(s)?;
107                f.write_char(']')
108            }
109            PageSegment::CatchAll(s) => {
110                f.write_str("[...")?;
111                f.write_str(s)?;
112                f.write_char(']')
113            }
114            PageSegment::OptionalCatchAll(s) => {
115                f.write_str("[[...")?;
116                f.write_str(s)?;
117                f.write_str("]]")
118            }
119            PageSegment::Group(s) => {
120                f.write_char('(')?;
121                f.write_str(s)?;
122                f.write_char(')')
123            }
124            PageSegment::Parallel(s) => {
125                f.write_char('@')?;
126                f.write_str(s)
127            }
128            PageSegment::PageType(s) => Display::fmt(s, f),
129        }
130    }
131}
132
133#[derive(
134    Clone,
135    Debug,
136    Hash,
137    PartialEq,
138    Eq,
139    PartialOrd,
140    Ord,
141    TaskInput,
142    TraceRawVcs,
143    NonLocalValue,
144    Encode,
145    Decode,
146)]
147pub enum PageType {
148    Page,
149    Route,
150}
151
152impl Display for PageType {
153    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
154        f.write_str(match self {
155            PageType::Page => "page",
156            PageType::Route => "route",
157        })
158    }
159}
160
161/// Describes the pathname including all internal modifiers such as
162/// intercepting routes, parallel routes and route/page suffixes that are not
163/// part of the pathname.
164#[derive(
165    Clone,
166    Debug,
167    Hash,
168    PartialEq,
169    Eq,
170    Default,
171    TaskInput,
172    TraceRawVcs,
173    NonLocalValue,
174    Encode,
175    Decode,
176)]
177pub struct AppPage(pub Vec<PageSegment>);
178
179impl AppPage {
180    pub fn new() -> Self {
181        Self::default()
182    }
183
184    pub fn push(&mut self, segment: PageSegment) -> Result<()> {
185        let has_catchall = self.0.iter().any(|segment| {
186            matches!(
187                segment,
188                PageSegment::CatchAll(..) | PageSegment::OptionalCatchAll(..)
189            )
190        });
191
192        if has_catchall
193            && matches!(
194                segment,
195                PageSegment::Static(..)
196                    | PageSegment::Dynamic(..)
197                    | PageSegment::CatchAll(..)
198                    | PageSegment::OptionalCatchAll(..)
199            )
200        {
201            bail!(
202                "Invalid segment {:?}, catch all segment must be the last segment modifying the \
203                 path (segments: {:?})",
204                segment,
205                self.0
206            )
207        }
208
209        if self.is_complete() {
210            bail!(
211                "Invalid segment {:?}, this page path already has the final PageType appended \
212                 (segments: {:?})",
213                segment,
214                self.0
215            )
216        }
217
218        self.0.push(segment);
219        Ok(())
220    }
221
222    pub fn push_str(&mut self, segment: &str) -> Result<()> {
223        if segment.is_empty() {
224            return Ok(());
225        }
226
227        self.push(PageSegment::parse(segment)?)
228    }
229
230    pub fn clone_push(&self, segment: PageSegment) -> Result<Self> {
231        let mut cloned = self.clone();
232        cloned.push(segment)?;
233        Ok(cloned)
234    }
235
236    pub fn clone_push_str(&self, segment: &str) -> Result<Self> {
237        let mut cloned = self.clone();
238        cloned.push_str(segment)?;
239        Ok(cloned)
240    }
241
242    pub fn parse(page: &str) -> Result<Self> {
243        let mut app_page = Self::new();
244
245        for segment in page.split('/') {
246            app_page.push_str(segment)?;
247        }
248
249        if let Some(last) = app_page.0.last_mut()
250            && let PageSegment::Static(last_name) = &*last
251        {
252            // Next.js internals sometimes omit extensions when creating synthetic page entries
253            if last_name == "page" || last_name.starts_with("page.") {
254                *last = PageSegment::PageType(PageType::Page);
255            } else if last_name == "route" || last_name.starts_with("route.") {
256                *last = PageSegment::PageType(PageType::Route);
257            }
258            // can also be metadata (and be neither Page nor Route)
259        }
260
261        Ok(app_page)
262    }
263
264    pub fn is_root(&self) -> bool {
265        self.0.is_empty()
266    }
267
268    pub fn is_complete(&self) -> bool {
269        matches!(self.0.last(), Some(PageSegment::PageType(..)))
270    }
271
272    /// The `PageType` is the last segment for completed pages. We need to find
273    /// the last segment that is not a `PageType`, `Group`, or `Parallel`
274    /// segment, because these do not inform the routing structure.
275    pub fn get_last_routing_segment(&self) -> Option<&PageSegment> {
276        self.0.iter().rev().find(|segment| {
277            !matches!(
278                segment,
279                PageSegment::PageType(_) | PageSegment::Group(_) | PageSegment::Parallel(_)
280            )
281        })
282    }
283
284    pub fn is_catchall(&self) -> bool {
285        matches!(
286            self.get_last_routing_segment(),
287            Some(PageSegment::CatchAll(_) | PageSegment::OptionalCatchAll(_))
288        )
289    }
290
291    pub fn is_intercepting(&self) -> bool {
292        let segment = if self.is_complete() {
293            // The `PageType` is the last segment for completed pages.
294            self.0.iter().nth_back(1)
295        } else {
296            self.0.last()
297        };
298
299        matches!(
300            segment,
301            Some(PageSegment::Static(segment))
302                if segment.starts_with("(.)")
303                    || segment.starts_with("(..)")
304                    || segment.starts_with("(...)")
305        )
306    }
307
308    /// Returns true if there is only one segment and it is a group.
309    pub fn is_first_layer_group_route(&self) -> bool {
310        self.0.len() == 1 && matches!(self.0.last(), Some(PageSegment::Group(_)))
311    }
312
313    pub fn complete(&self, page_type: PageType) -> Result<Self> {
314        self.clone_push(PageSegment::PageType(page_type))
315    }
316}
317
318impl Display for AppPage {
319    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
320        if self.0.is_empty() {
321            return f.write_char('/');
322        }
323
324        for segment in &self.0 {
325            f.write_char('/')?;
326            Display::fmt(segment, f)?;
327        }
328
329        Ok(())
330    }
331}
332
333impl Deref for AppPage {
334    type Target = [PageSegment];
335
336    fn deref(&self) -> &Self::Target {
337        &self.0
338    }
339}
340
341impl Ord for AppPage {
342    fn cmp(&self, other: &Self) -> Ordering {
343        // next.js does some weird stuff when looking up routes, so we have to emit the
344        // correct path (shortest segments, but alphabetically the last).
345        // https://github.com/vercel/next.js/blob/194311d8c96144d68e65cd9abb26924d25978da7/packages/next/src/server/base-server.ts#L3003
346        self.len().cmp(&other.len()).then(other.0.cmp(&self.0))
347    }
348}
349
350impl PartialOrd for AppPage {
351    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
352        Some(self.cmp(other))
353    }
354}
355
356/// Path segments for a router path (not including parallel routes and groups).
357///
358/// Also see [AppPath].
359#[derive(
360    Clone,
361    Debug,
362    Hash,
363    PartialEq,
364    Eq,
365    PartialOrd,
366    Ord,
367    TaskInput,
368    TraceRawVcs,
369    NonLocalValue,
370    Encode,
371    Decode,
372)]
373pub enum PathSegment {
374    /// e.g. `/dashboard`
375    Static(RcStr),
376    /// e.g. `/[id]`
377    Dynamic(RcStr),
378    /// e.g. `/[...slug]`
379    CatchAll(RcStr),
380    /// e.g. `/[[...slug]]`
381    OptionalCatchAll(RcStr),
382}
383
384impl Display for PathSegment {
385    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
386        match self {
387            PathSegment::Static(s) => f.write_str(s),
388            PathSegment::Dynamic(s) => {
389                f.write_char('[')?;
390                f.write_str(s)?;
391                f.write_char(']')
392            }
393            PathSegment::CatchAll(s) => {
394                f.write_str("[...")?;
395                f.write_str(s)?;
396                f.write_char(']')
397            }
398            PathSegment::OptionalCatchAll(s) => {
399                f.write_str("[[...")?;
400                f.write_str(s)?;
401                f.write_str("]]")
402            }
403        }
404    }
405}
406
407/// The pathname (including dynamic placeholders) for the next.js router to
408/// resolve.
409///
410/// Does not include internal modifiers as it's the equivalent of the http
411/// request path.
412#[derive(
413    Clone,
414    Debug,
415    Hash,
416    PartialEq,
417    Eq,
418    Default,
419    TaskInput,
420    TraceRawVcs,
421    NonLocalValue,
422    Encode,
423    Decode,
424)]
425pub struct AppPath(pub Vec<PathSegment>);
426
427impl AppPath {
428    pub fn is_dynamic(&self) -> bool {
429        self.iter().any(|segment| {
430            matches!(
431                (segment,),
432                (PathSegment::Dynamic(_)
433                    | PathSegment::CatchAll(_)
434                    | PathSegment::OptionalCatchAll(_),)
435            )
436        })
437    }
438
439    pub fn is_root(&self) -> bool {
440        self.0.is_empty()
441    }
442
443    pub fn is_catchall(&self) -> bool {
444        // can only be the last segment.
445        matches!(
446            self.last(),
447            Some(PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_))
448        )
449    }
450
451    pub fn contains(&self, other: &AppPath) -> bool {
452        // TODO: handle OptionalCatchAll properly.
453        for (i, segment) in other.0.iter().enumerate() {
454            let Some(self_segment) = self.0.get(i) else {
455                // other is longer than self
456                return false;
457            };
458
459            if self_segment == segment {
460                continue;
461            }
462
463            if matches!(
464                segment,
465                PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_)
466            ) {
467                return true;
468            }
469
470            return false;
471        }
472
473        true
474    }
475
476    /// Returns true if ANY segment in the entire path is an interception route.
477    /// This is different from `is_intercepting()` which only checks the last
478    /// segment.
479    pub fn contains_interception(&self) -> bool {
480        self.iter().any(|segment| {
481            matches!(
482                segment,
483                PathSegment::Static(s) if s.starts_with("(.)") || s.starts_with("(..)") || s.starts_with("(...)")
484            )
485        })
486    }
487}
488
489impl Deref for AppPath {
490    type Target = [PathSegment];
491
492    fn deref(&self) -> &Self::Target {
493        &self.0
494    }
495}
496
497impl Display for AppPath {
498    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
499        if self.0.is_empty() {
500            return f.write_char('/');
501        }
502
503        for segment in &self.0 {
504            f.write_char('/')?;
505            Display::fmt(segment, f)?;
506        }
507
508        Ok(())
509    }
510}
511
512impl Ord for AppPath {
513    fn cmp(&self, other: &Self) -> Ordering {
514        // next.js does some weird stuff when looking up routes, so we have to emit the
515        // correct path (shortest segments, but alphabetically the last).
516        // https://github.com/vercel/next.js/blob/194311d8c96144d68e65cd9abb26924d25978da7/packages/next/src/server/base-server.ts#L3003
517        self.len().cmp(&other.len()).then(other.0.cmp(&self.0))
518    }
519}
520
521impl PartialOrd for AppPath {
522    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
523        Some(self.cmp(other))
524    }
525}
526
527impl From<AppPage> for AppPath {
528    fn from(value: AppPage) -> Self {
529        AppPath(
530            value
531                .0
532                .into_iter()
533                .filter_map(|segment| match segment {
534                    PageSegment::Static(s) => Some(PathSegment::Static(s)),
535                    PageSegment::Dynamic(s) => Some(PathSegment::Dynamic(s)),
536                    PageSegment::CatchAll(s) => Some(PathSegment::CatchAll(s)),
537                    PageSegment::OptionalCatchAll(s) => Some(PathSegment::OptionalCatchAll(s)),
538                    _ => None,
539                })
540                .collect(),
541        )
542    }
543}
544
545#[cfg(test)]
546mod test {
547    use crate::next_app::{AppPage, PageSegment, PageType};
548
549    #[test]
550    fn test_normalize_metadata_route() {
551        assert_eq!(
552            AppPage::parse("(group)/foo/@par/bar/page.tsx").unwrap(),
553            AppPage(vec![
554                PageSegment::Group("group".into()),
555                PageSegment::Static("foo".into()),
556                PageSegment::Parallel("par".into()),
557                PageSegment::Static("bar".into()),
558                PageSegment::PageType(PageType::Page),
559            ])
560        );
561        assert_eq!(
562            AppPage::parse("(group)/foo/@par/bar/page").unwrap(),
563            AppPage(vec![
564                PageSegment::Group("group".into()),
565                PageSegment::Static("foo".into()),
566                PageSegment::Parallel("par".into()),
567                PageSegment::Static("bar".into()),
568                PageSegment::PageType(PageType::Page),
569            ])
570        );
571
572        assert_eq!(
573            AppPage::parse("(group)/foo/@par/bar/route.tsx").unwrap(),
574            AppPage(vec![
575                PageSegment::Group("group".into()),
576                PageSegment::Static("foo".into()),
577                PageSegment::Parallel("par".into()),
578                PageSegment::Static("bar".into()),
579                PageSegment::PageType(PageType::Route),
580            ])
581        );
582        assert_eq!(
583            AppPage::parse("(group)/foo/@par/bar/route").unwrap(),
584            AppPage(vec![
585                PageSegment::Group("group".into()),
586                PageSegment::Static("foo".into()),
587                PageSegment::Parallel("par".into()),
588                PageSegment::Static("bar".into()),
589                PageSegment::PageType(PageType::Route),
590            ])
591        );
592
593        assert_eq!(
594            AppPage::parse("foo/sitemap").unwrap(),
595            AppPage(vec![
596                PageSegment::Static("foo".into()),
597                PageSegment::Static("sitemap".into()),
598            ])
599        );
600
601        assert_eq!(
602            AppPage::parse("foo/robots.txt").unwrap(),
603            AppPage(vec![
604                PageSegment::Static("foo".into()),
605                PageSegment::Static("robots.txt".into()),
606            ])
607        );
608    }
609}