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 serde::{Deserialize, Serialize};
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    Serialize,
33    Deserialize,
34    PartialEq,
35    Eq,
36    PartialOrd,
37    Ord,
38    TaskInput,
39    TraceRawVcs,
40    NonLocalValue,
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    Serialize,
138    Deserialize,
139    PartialEq,
140    Eq,
141    PartialOrd,
142    Ord,
143    TaskInput,
144    TraceRawVcs,
145    NonLocalValue,
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    Serialize,
172    Deserialize,
173    TaskInput,
174    TraceRawVcs,
175    NonLocalValue,
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        Ok(app_page)
250    }
251
252    pub fn is_root(&self) -> bool {
253        self.0.is_empty()
254    }
255
256    pub fn is_complete(&self) -> bool {
257        matches!(self.0.last(), Some(PageSegment::PageType(..)))
258    }
259
260    /// The `PageType` is the last segment for completed pages. We need to find
261    /// the last segment that is not a `PageType`, `Group`, or `Parallel`
262    /// segment, because these do not inform the routing structure.
263    pub fn get_last_routing_segment(&self) -> Option<&PageSegment> {
264        self.0.iter().rev().find(|segment| {
265            !matches!(
266                segment,
267                PageSegment::PageType(_) | PageSegment::Group(_) | PageSegment::Parallel(_)
268            )
269        })
270    }
271
272    pub fn is_catchall(&self) -> bool {
273        matches!(
274            self.get_last_routing_segment(),
275            Some(PageSegment::CatchAll(_) | PageSegment::OptionalCatchAll(_))
276        )
277    }
278
279    pub fn is_intercepting(&self) -> bool {
280        let segment = if self.is_complete() {
281            // The `PageType` is the last segment for completed pages.
282            self.0.iter().nth_back(1)
283        } else {
284            self.0.last()
285        };
286
287        matches!(
288            segment,
289            Some(PageSegment::Static(segment))
290                if segment.starts_with("(.)")
291                    || segment.starts_with("(..)")
292                    || segment.starts_with("(...)")
293        )
294    }
295
296    /// Returns true if there is only one segment and it is a group.
297    pub fn is_first_layer_group_route(&self) -> bool {
298        self.0.len() == 1 && matches!(self.0.last(), Some(PageSegment::Group(_)))
299    }
300
301    pub fn complete(&self, page_type: PageType) -> Result<Self> {
302        self.clone_push(PageSegment::PageType(page_type))
303    }
304}
305
306impl Display for AppPage {
307    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
308        if self.0.is_empty() {
309            return f.write_char('/');
310        }
311
312        for segment in &self.0 {
313            f.write_char('/')?;
314            Display::fmt(segment, f)?;
315        }
316
317        Ok(())
318    }
319}
320
321impl Deref for AppPage {
322    type Target = [PageSegment];
323
324    fn deref(&self) -> &Self::Target {
325        &self.0
326    }
327}
328
329impl Ord for AppPage {
330    fn cmp(&self, other: &Self) -> Ordering {
331        // next.js does some weird stuff when looking up routes, so we have to emit the
332        // correct path (shortest segments, but alphabetically the last).
333        // https://github.com/vercel/next.js/blob/194311d8c96144d68e65cd9abb26924d25978da7/packages/next/src/server/base-server.ts#L3003
334        self.len().cmp(&other.len()).then(other.0.cmp(&self.0))
335    }
336}
337
338impl PartialOrd for AppPage {
339    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
340        Some(self.cmp(other))
341    }
342}
343
344/// Path segments for a router path (not including parallel routes and groups).
345///
346/// Also see [AppPath].
347#[derive(
348    Clone,
349    Debug,
350    Hash,
351    Serialize,
352    Deserialize,
353    PartialEq,
354    Eq,
355    PartialOrd,
356    Ord,
357    TaskInput,
358    TraceRawVcs,
359    NonLocalValue,
360)]
361pub enum PathSegment {
362    /// e.g. `/dashboard`
363    Static(RcStr),
364    /// e.g. `/[id]`
365    Dynamic(RcStr),
366    /// e.g. `/[...slug]`
367    CatchAll(RcStr),
368    /// e.g. `/[[...slug]]`
369    OptionalCatchAll(RcStr),
370}
371
372impl Display for PathSegment {
373    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
374        match self {
375            PathSegment::Static(s) => f.write_str(s),
376            PathSegment::Dynamic(s) => {
377                f.write_char('[')?;
378                f.write_str(s)?;
379                f.write_char(']')
380            }
381            PathSegment::CatchAll(s) => {
382                f.write_str("[...")?;
383                f.write_str(s)?;
384                f.write_char(']')
385            }
386            PathSegment::OptionalCatchAll(s) => {
387                f.write_str("[[...")?;
388                f.write_str(s)?;
389                f.write_str("]]")
390            }
391        }
392    }
393}
394
395/// The pathname (including dynamic placeholders) for the next.js router to
396/// resolve.
397///
398/// Does not include internal modifiers as it's the equivalent of the http
399/// request path.
400#[derive(
401    Clone,
402    Debug,
403    Hash,
404    PartialEq,
405    Eq,
406    Default,
407    Serialize,
408    Deserialize,
409    TaskInput,
410    TraceRawVcs,
411    NonLocalValue,
412)]
413pub struct AppPath(pub Vec<PathSegment>);
414
415impl AppPath {
416    pub fn is_dynamic(&self) -> bool {
417        self.iter().any(|segment| {
418            matches!(
419                (segment,),
420                (PathSegment::Dynamic(_)
421                    | PathSegment::CatchAll(_)
422                    | PathSegment::OptionalCatchAll(_),)
423            )
424        })
425    }
426
427    pub fn is_root(&self) -> bool {
428        self.0.is_empty()
429    }
430
431    pub fn is_catchall(&self) -> bool {
432        // can only be the last segment.
433        matches!(
434            self.last(),
435            Some(PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_))
436        )
437    }
438
439    pub fn contains(&self, other: &AppPath) -> bool {
440        // TODO: handle OptionalCatchAll properly.
441        for (i, segment) in other.0.iter().enumerate() {
442            let Some(self_segment) = self.0.get(i) else {
443                // other is longer than self
444                return false;
445            };
446
447            if self_segment == segment {
448                continue;
449            }
450
451            if matches!(
452                segment,
453                PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_)
454            ) {
455                return true;
456            }
457
458            return false;
459        }
460
461        true
462    }
463}
464
465impl Deref for AppPath {
466    type Target = [PathSegment];
467
468    fn deref(&self) -> &Self::Target {
469        &self.0
470    }
471}
472
473impl Display for AppPath {
474    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
475        if self.0.is_empty() {
476            return f.write_char('/');
477        }
478
479        for segment in &self.0 {
480            f.write_char('/')?;
481            Display::fmt(segment, f)?;
482        }
483
484        Ok(())
485    }
486}
487
488impl Ord for AppPath {
489    fn cmp(&self, other: &Self) -> Ordering {
490        // next.js does some weird stuff when looking up routes, so we have to emit the
491        // correct path (shortest segments, but alphabetically the last).
492        // https://github.com/vercel/next.js/blob/194311d8c96144d68e65cd9abb26924d25978da7/packages/next/src/server/base-server.ts#L3003
493        self.len().cmp(&other.len()).then(other.0.cmp(&self.0))
494    }
495}
496
497impl PartialOrd for AppPath {
498    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
499        Some(self.cmp(other))
500    }
501}
502
503impl From<AppPage> for AppPath {
504    fn from(value: AppPage) -> Self {
505        AppPath(
506            value
507                .0
508                .into_iter()
509                .filter_map(|segment| match segment {
510                    PageSegment::Static(s) => Some(PathSegment::Static(s)),
511                    PageSegment::Dynamic(s) => Some(PathSegment::Dynamic(s)),
512                    PageSegment::CatchAll(s) => Some(PathSegment::CatchAll(s)),
513                    PageSegment::OptionalCatchAll(s) => Some(PathSegment::OptionalCatchAll(s)),
514                    _ => None,
515                })
516                .collect(),
517        )
518    }
519}