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