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 is_catchall(&self) -> bool {
261 let segment = if self.is_complete() {
262 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 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 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#[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 Static(RcStr),
354 Dynamic(RcStr),
356 CatchAll(RcStr),
358 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#[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 matches!(
424 self.last(),
425 Some(PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_))
426 )
427 }
428
429 pub fn contains(&self, other: &AppPath) -> bool {
430 for (i, segment) in other.0.iter().enumerate() {
432 let Some(self_segment) = self.0.get(i) else {
433 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 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}