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#[turbo_tasks::task_input]
32#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, TraceRawVcs, Encode, Decode)]
33pub enum PageSegment {
34 Static(RcStr),
36 Dynamic(RcStr),
38 CatchAll(RcStr),
40 OptionalCatchAll(RcStr),
42 Group(RcStr),
44 Parallel(RcStr),
46 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#[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 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 }
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 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 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 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 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#[turbo_tasks::task_input]
328#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, TraceRawVcs, Encode, Decode)]
329pub enum PathSegment {
330 Static(RcStr),
332 Dynamic(RcStr),
334 CatchAll(RcStr),
336 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#[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 matches!(
391 self.last(),
392 Some(PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_))
393 )
394 }
395
396 pub fn contains(&self, other: &AppPath) -> bool {
397 for (i, segment) in other.0.iter().enumerate() {
399 let Some(self_segment) = self.0.get(i) else {
400 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 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 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}