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#[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 Static(RcStr),
45 Dynamic(RcStr),
47 CatchAll(RcStr),
49 OptionalCatchAll(RcStr),
51 Group(RcStr),
53 Parallel(RcStr),
55 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#[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 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 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 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 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#[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 Static(RcStr),
364 Dynamic(RcStr),
366 CatchAll(RcStr),
368 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#[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 matches!(
434 self.last(),
435 Some(PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_))
436 )
437 }
438
439 pub fn contains(&self, other: &AppPath) -> bool {
440 for (i, segment) in other.0.iter().enumerate() {
442 let Some(self_segment) = self.0.get(i) else {
443 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 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}