1use std::collections::BTreeMap;
2
3use anyhow::{Context, Result, bail};
4use indexmap::map::{Entry, OccupiedEntry};
5use rustc_hash::FxHashMap;
6use serde::{Deserialize, Serialize};
7use tracing::Instrument;
8use turbo_rcstr::{RcStr, rcstr};
9use turbo_tasks::{
10 FxIndexMap, NonLocalValue, ResolvedVc, TaskInput, TryJoinIterExt, ValueDefault, Vc,
11 debug::ValueDebugFormat, fxindexmap, trace::TraceRawVcs,
12};
13use turbo_tasks_fs::{DirectoryContent, DirectoryEntry, FileSystemEntryType, FileSystemPath};
14use turbopack_core::issue::{
15 Issue, IssueExt, IssueSeverity, IssueStage, OptionStyledString, StyledString,
16};
17
18use crate::{
19 next_app::{
20 AppPage, AppPath, PageSegment, PageType,
21 metadata::{
22 GlobalMetadataFileMatch, MetadataFileMatch, match_global_metadata_file,
23 match_local_metadata_file, normalize_metadata_route,
24 },
25 },
26 next_import_map::get_next_package,
27};
28
29fn normalize_underscore(string: &str) -> String {
32 string.replace("%5F", "_")
33}
34
35#[turbo_tasks::value]
37#[derive(Default, Debug, Clone)]
38pub struct AppDirModules {
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub page: Option<FileSystemPath>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub layout: Option<FileSystemPath>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub error: Option<FileSystemPath>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub global_error: Option<FileSystemPath>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub global_not_found: Option<FileSystemPath>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub loading: Option<FileSystemPath>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub template: Option<FileSystemPath>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub forbidden: Option<FileSystemPath>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub unauthorized: Option<FileSystemPath>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub not_found: Option<FileSystemPath>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub default: Option<FileSystemPath>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub route: Option<FileSystemPath>,
63 #[serde(skip_serializing_if = "Metadata::is_empty", default)]
64 pub metadata: Metadata,
65}
66
67impl AppDirModules {
68 fn without_leaves(&self) -> Self {
69 Self {
70 page: None,
71 layout: self.layout.clone(),
72 error: self.error.clone(),
73 global_error: self.global_error.clone(),
74 global_not_found: self.global_not_found.clone(),
75 loading: self.loading.clone(),
76 template: self.template.clone(),
77 not_found: self.not_found.clone(),
78 forbidden: self.forbidden.clone(),
79 unauthorized: self.unauthorized.clone(),
80 default: None,
81 route: None,
82 metadata: self.metadata.clone(),
83 }
84 }
85}
86
87#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, TraceRawVcs, NonLocalValue)]
89pub enum MetadataWithAltItem {
90 Static {
91 path: FileSystemPath,
92 alt_path: Option<FileSystemPath>,
93 },
94 Dynamic {
95 path: FileSystemPath,
96 },
97}
98
99#[derive(
101 Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq, TaskInput, TraceRawVcs, NonLocalValue,
102)]
103pub enum MetadataItem {
104 Static { path: FileSystemPath },
105 Dynamic { path: FileSystemPath },
106}
107
108#[turbo_tasks::function]
109pub async fn get_metadata_route_name(meta: MetadataItem) -> Result<Vc<RcStr>> {
110 Ok(match meta {
111 MetadataItem::Static { path } => Vc::cell(path.file_name().into()),
112 MetadataItem::Dynamic { path } => {
113 let Some(stem) = path.file_stem() else {
114 bail!(
115 "unable to resolve file stem for metadata item at {}",
116 path.value_to_string().await?
117 );
118 };
119
120 match stem {
121 "manifest" => Vc::cell(rcstr!("manifest.webmanifest")),
122 _ => Vc::cell(RcStr::from(stem)),
123 }
124 }
125 })
126}
127
128impl MetadataItem {
129 pub fn into_path(self) -> FileSystemPath {
130 match self {
131 MetadataItem::Static { path } => path,
132 MetadataItem::Dynamic { path } => path,
133 }
134 }
135}
136
137impl From<MetadataWithAltItem> for MetadataItem {
138 fn from(value: MetadataWithAltItem) -> Self {
139 match value {
140 MetadataWithAltItem::Static { path, .. } => MetadataItem::Static { path },
141 MetadataWithAltItem::Dynamic { path } => MetadataItem::Dynamic { path },
142 }
143 }
144}
145
146#[derive(
148 Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, TraceRawVcs, NonLocalValue,
149)]
150pub struct Metadata {
151 #[serde(skip_serializing_if = "Vec::is_empty", default)]
152 pub icon: Vec<MetadataWithAltItem>,
153 #[serde(skip_serializing_if = "Vec::is_empty", default)]
154 pub apple: Vec<MetadataWithAltItem>,
155 #[serde(skip_serializing_if = "Vec::is_empty", default)]
156 pub twitter: Vec<MetadataWithAltItem>,
157 #[serde(skip_serializing_if = "Vec::is_empty", default)]
158 pub open_graph: Vec<MetadataWithAltItem>,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub sitemap: Option<MetadataItem>,
161 #[serde(skip_serializing_if = "Option::is_none")]
169 pub base_page: Option<AppPage>,
170}
171
172impl Metadata {
173 pub fn is_empty(&self) -> bool {
174 let Metadata {
175 icon,
176 apple,
177 twitter,
178 open_graph,
179 sitemap,
180 base_page: _,
181 } = self;
182 icon.is_empty()
183 && apple.is_empty()
184 && twitter.is_empty()
185 && open_graph.is_empty()
186 && sitemap.is_none()
187 }
188}
189
190#[turbo_tasks::value]
192#[derive(Default, Clone, Debug)]
193pub struct GlobalMetadata {
194 #[serde(skip_serializing_if = "Option::is_none")]
195 pub favicon: Option<MetadataItem>,
196 #[serde(skip_serializing_if = "Option::is_none")]
197 pub robots: Option<MetadataItem>,
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub manifest: Option<MetadataItem>,
200}
201
202impl GlobalMetadata {
203 pub fn is_empty(&self) -> bool {
204 let GlobalMetadata {
205 favicon,
206 robots,
207 manifest,
208 } = self;
209 favicon.is_none() && robots.is_none() && manifest.is_none()
210 }
211}
212
213#[turbo_tasks::value]
214#[derive(Debug)]
215pub struct DirectoryTree {
216 pub subdirectories: BTreeMap<RcStr, ResolvedVc<DirectoryTree>>,
218 pub modules: AppDirModules,
219}
220
221#[turbo_tasks::value]
222#[derive(Clone, Debug)]
223struct PlainDirectoryTree {
224 pub subdirectories: BTreeMap<RcStr, PlainDirectoryTree>,
226 pub modules: AppDirModules,
227}
228
229#[turbo_tasks::value_impl]
230impl DirectoryTree {
231 #[turbo_tasks::function]
232 pub async fn into_plain(&self) -> Result<Vc<PlainDirectoryTree>> {
233 let mut subdirectories = BTreeMap::new();
234
235 for (name, subdirectory) in &self.subdirectories {
236 subdirectories.insert(name.clone(), subdirectory.into_plain().owned().await?);
237 }
238
239 Ok(PlainDirectoryTree {
240 subdirectories,
241 modules: self.modules.clone(),
242 }
243 .cell())
244 }
245}
246
247#[turbo_tasks::value(transparent)]
248pub struct OptionAppDir(Option<FileSystemPath>);
249
250#[turbo_tasks::function]
252pub async fn find_app_dir(project_path: FileSystemPath) -> Result<Vc<OptionAppDir>> {
253 let app = project_path.join("app")?;
254 let src_app = project_path.join("src/app")?;
255 let app_dir = if *app.get_type().await? == FileSystemEntryType::Directory {
256 app
257 } else if *src_app.get_type().await? == FileSystemEntryType::Directory {
258 src_app
259 } else {
260 return Ok(Vc::cell(None));
261 };
262
263 Ok(Vc::cell(Some(app_dir)))
264}
265
266#[turbo_tasks::function]
267async fn get_directory_tree(
268 dir: FileSystemPath,
269 page_extensions: Vc<Vec<RcStr>>,
270) -> Result<Vc<DirectoryTree>> {
271 let span = {
272 let dir = dir.value_to_string().await?.to_string();
273 tracing::info_span!("read app directory tree", name = dir)
274 };
275 get_directory_tree_internal(dir, page_extensions)
276 .instrument(span)
277 .await
278}
279
280async fn get_directory_tree_internal(
281 dir: FileSystemPath,
282 page_extensions: Vc<Vec<RcStr>>,
283) -> Result<Vc<DirectoryTree>> {
284 let DirectoryContent::Entries(entries) = &*dir.read_dir().await? else {
285 return Ok(DirectoryTree {
289 subdirectories: Default::default(),
290 modules: AppDirModules::default(),
291 }
292 .cell());
293 };
294 let page_extensions_value = page_extensions.await?;
295
296 let mut subdirectories = BTreeMap::new();
297 let mut modules = AppDirModules::default();
298
299 let mut metadata_icon = Vec::new();
300 let mut metadata_apple = Vec::new();
301 let mut metadata_open_graph = Vec::new();
302 let mut metadata_twitter = Vec::new();
303
304 for (basename, entry) in entries {
305 let entry = entry.clone().resolve_symlink().await?;
306 match entry {
307 DirectoryEntry::File(file) => {
308 if basename.ends_with(".d.ts") {
310 continue;
311 }
312 if let Some((stem, ext)) = basename.split_once('.')
313 && page_extensions_value.iter().any(|e| e == ext)
314 {
315 match stem {
316 "page" => modules.page = Some(file.clone()),
317 "layout" => modules.layout = Some(file.clone()),
318 "error" => modules.error = Some(file.clone()),
319 "global-error" => modules.global_error = Some(file.clone()),
320 "global-not-found" => modules.global_not_found = Some(file.clone()),
321 "loading" => modules.loading = Some(file.clone()),
322 "template" => modules.template = Some(file.clone()),
323 "forbidden" => modules.forbidden = Some(file.clone()),
324 "unauthorized" => modules.unauthorized = Some(file.clone()),
325 "not-found" => modules.not_found = Some(file.clone()),
326 "default" => modules.default = Some(file.clone()),
327 "route" => modules.route = Some(file.clone()),
328 _ => {}
329 }
330 }
331
332 let Some(MetadataFileMatch {
333 metadata_type,
334 number,
335 dynamic,
336 }) = match_local_metadata_file(basename.as_str(), &page_extensions_value)
337 else {
338 continue;
339 };
340
341 let entry = match metadata_type {
342 "icon" => &mut metadata_icon,
343 "apple-icon" => &mut metadata_apple,
344 "twitter-image" => &mut metadata_twitter,
345 "opengraph-image" => &mut metadata_open_graph,
346 "sitemap" => {
347 if dynamic {
348 modules.metadata.sitemap = Some(MetadataItem::Dynamic { path: file });
349 } else {
350 modules.metadata.sitemap = Some(MetadataItem::Static { path: file });
351 }
352 continue;
353 }
354 _ => continue,
355 };
356
357 if dynamic {
358 entry.push((number, MetadataWithAltItem::Dynamic { path: file }));
359 continue;
360 }
361
362 let file_name = file.file_name();
363 let basename = file_name
364 .rsplit_once('.')
365 .map_or(file_name, |(basename, _)| basename);
366 let alt_path = file.parent().join(&format!("{basename}.alt.txt"))?;
367 let alt_path = matches!(&*alt_path.get_type().await?, FileSystemEntryType::File)
368 .then_some(alt_path);
369
370 entry.push((
371 number,
372 MetadataWithAltItem::Static {
373 path: file,
374 alt_path,
375 },
376 ));
377 }
378 DirectoryEntry::Directory(dir) => {
379 if !basename.starts_with('_') {
381 let result = get_directory_tree(dir.clone(), page_extensions)
382 .to_resolved()
383 .await?;
384 subdirectories.insert(basename.clone(), result);
385 }
386 }
387 _ => {}
389 }
390 }
391
392 fn sort<T>(mut list: Vec<(Option<u32>, T)>) -> Vec<T> {
393 list.sort_by_key(|(num, _)| *num);
394 list.into_iter().map(|(_, item)| item).collect()
395 }
396
397 modules.metadata.icon = sort(metadata_icon);
398 modules.metadata.apple = sort(metadata_apple);
399 modules.metadata.twitter = sort(metadata_twitter);
400 modules.metadata.open_graph = sort(metadata_open_graph);
401
402 Ok(DirectoryTree {
403 subdirectories,
404 modules,
405 }
406 .cell())
407}
408
409#[turbo_tasks::value]
410#[derive(Debug, Clone)]
411pub struct AppPageLoaderTree {
412 pub page: AppPage,
413 pub segment: RcStr,
414 pub parallel_routes: FxIndexMap<RcStr, AppPageLoaderTree>,
415 pub modules: AppDirModules,
416 pub global_metadata: ResolvedVc<GlobalMetadata>,
417}
418
419impl AppPageLoaderTree {
420 pub fn has_page(&self) -> bool {
422 if &*self.segment == "__PAGE__" {
423 return true;
424 }
425
426 for (_, tree) in &self.parallel_routes {
427 if tree.has_page() {
428 return true;
429 }
430 }
431
432 false
433 }
434
435 pub fn has_only_catchall(&self) -> bool {
438 if &*self.segment == "__PAGE__" && !self.page.is_catchall() {
439 return false;
440 }
441
442 for (_, tree) in &self.parallel_routes {
443 if !tree.has_only_catchall() {
444 return false;
445 }
446 }
447
448 true
449 }
450
451 pub fn is_intercepting(&self) -> bool {
453 if self.page.is_intercepting() && self.has_page() {
454 return true;
455 }
456
457 for (_, tree) in &self.parallel_routes {
458 if tree.is_intercepting() {
459 return true;
460 }
461 }
462
463 false
464 }
465
466 pub fn get_specificity(&self) -> usize {
469 if &*self.segment == "__PAGE__" {
470 return AppPath::from(self.page.clone()).len();
471 }
472
473 let mut specificity = 0;
474
475 for (_, tree) in &self.parallel_routes {
476 specificity = specificity.max(tree.get_specificity());
477 }
478
479 specificity
480 }
481}
482
483#[turbo_tasks::value(transparent)]
484pub struct FileSystemPathVec(Vec<FileSystemPath>);
485
486#[turbo_tasks::value_impl]
487impl ValueDefault for FileSystemPathVec {
488 #[turbo_tasks::function]
489 fn value_default() -> Vc<Self> {
490 Vc::cell(Vec::new())
491 }
492}
493
494#[derive(
495 Clone,
496 PartialEq,
497 Eq,
498 Hash,
499 Serialize,
500 Deserialize,
501 TraceRawVcs,
502 ValueDebugFormat,
503 Debug,
504 TaskInput,
505 NonLocalValue,
506)]
507pub enum Entrypoint {
508 AppPage {
509 pages: Vec<AppPage>,
510 loader_tree: ResolvedVc<AppPageLoaderTree>,
511 },
512 AppRoute {
513 page: AppPage,
514 path: FileSystemPath,
515 root_layouts: ResolvedVc<FileSystemPathVec>,
516 },
517 AppMetadata {
518 page: AppPage,
519 metadata: MetadataItem,
520 },
521}
522
523impl Entrypoint {
524 pub fn page(&self) -> &AppPage {
525 match self {
526 Entrypoint::AppPage { pages, .. } => pages.first().unwrap(),
527 Entrypoint::AppRoute { page, .. } => page,
528 Entrypoint::AppMetadata { page, .. } => page,
529 }
530 }
531}
532
533#[turbo_tasks::value(transparent)]
534pub struct Entrypoints(FxIndexMap<AppPath, Entrypoint>);
535
536fn is_parallel_route(name: &str) -> bool {
537 name.starts_with('@')
538}
539
540fn is_group_route(name: &str) -> bool {
541 name.starts_with('(') && name.ends_with(')')
542}
543
544fn match_parallel_route(name: &str) -> Option<&str> {
545 name.strip_prefix('@')
546}
547
548fn conflict_issue(
549 app_dir: FileSystemPath,
550 e: &'_ OccupiedEntry<'_, AppPath, Entrypoint>,
551 a: &str,
552 b: &str,
553 value_a: &AppPage,
554 value_b: &AppPage,
555) {
556 let item_names = if a == b {
557 format!("{a}s")
558 } else {
559 format!("{a} and {b}")
560 };
561
562 DirectoryTreeIssue {
563 app_dir,
564 message: StyledString::Text(
565 format!(
566 "Conflicting {} at {}: {a} at {value_a} and {b} at {value_b}",
567 item_names,
568 e.key(),
569 )
570 .into(),
571 )
572 .resolved_cell(),
573 severity: IssueSeverity::Error,
574 }
575 .resolved_cell()
576 .emit();
577}
578
579fn add_app_page(
580 app_dir: FileSystemPath,
581 result: &mut FxIndexMap<AppPath, Entrypoint>,
582 page: AppPage,
583 loader_tree: ResolvedVc<AppPageLoaderTree>,
584) {
585 let mut e = match result.entry(page.clone().into()) {
586 Entry::Occupied(e) => e,
587 Entry::Vacant(e) => {
588 e.insert(Entrypoint::AppPage {
589 pages: vec![page],
590 loader_tree,
591 });
592 return;
593 }
594 };
595
596 let conflict = |existing_name: &str, existing_page: &AppPage| {
597 conflict_issue(app_dir, &e, "page", existing_name, &page, existing_page);
598 };
599
600 let value = e.get();
601 match value {
602 Entrypoint::AppPage {
603 pages: existing_pages,
604 loader_tree: existing_loader_tree,
605 } => {
606 if *existing_loader_tree != loader_tree {
609 conflict("page", existing_pages.first().unwrap());
610 }
611
612 let Entrypoint::AppPage {
613 pages: stored_pages,
614 ..
615 } = e.get_mut()
616 else {
617 unreachable!("Entrypoint::AppPage was already matched");
618 };
619
620 stored_pages.push(page);
621 stored_pages.sort();
622 }
623 Entrypoint::AppRoute {
624 page: existing_page,
625 ..
626 } => {
627 conflict("route", existing_page);
628 }
629 Entrypoint::AppMetadata {
630 page: existing_page,
631 ..
632 } => {
633 conflict("metadata", existing_page);
634 }
635 }
636}
637
638fn add_app_route(
639 app_dir: FileSystemPath,
640 result: &mut FxIndexMap<AppPath, Entrypoint>,
641 page: AppPage,
642 path: FileSystemPath,
643 root_layouts: ResolvedVc<FileSystemPathVec>,
644) {
645 let e = match result.entry(page.clone().into()) {
646 Entry::Occupied(e) => e,
647 Entry::Vacant(e) => {
648 e.insert(Entrypoint::AppRoute {
649 page,
650 path,
651 root_layouts,
652 });
653 return;
654 }
655 };
656
657 let conflict = |existing_name: &str, existing_page: &AppPage| {
658 conflict_issue(app_dir, &e, "route", existing_name, &page, existing_page);
659 };
660
661 let value = e.get();
662 match value {
663 Entrypoint::AppPage { pages, .. } => {
664 conflict("page", pages.first().unwrap());
665 }
666 Entrypoint::AppRoute {
667 page: existing_page,
668 ..
669 } => {
670 conflict("route", existing_page);
671 }
672 Entrypoint::AppMetadata {
673 page: existing_page,
674 ..
675 } => {
676 conflict("metadata", existing_page);
677 }
678 }
679}
680
681fn add_app_metadata_route(
682 app_dir: FileSystemPath,
683 result: &mut FxIndexMap<AppPath, Entrypoint>,
684 page: AppPage,
685 metadata: MetadataItem,
686) {
687 let e = match result.entry(page.clone().into()) {
688 Entry::Occupied(e) => e,
689 Entry::Vacant(e) => {
690 e.insert(Entrypoint::AppMetadata { page, metadata });
691 return;
692 }
693 };
694
695 let conflict = |existing_name: &str, existing_page: &AppPage| {
696 conflict_issue(app_dir, &e, "metadata", existing_name, &page, existing_page);
697 };
698
699 let value = e.get();
700 match value {
701 Entrypoint::AppPage { pages, .. } => {
702 conflict("page", pages.first().unwrap());
703 }
704 Entrypoint::AppRoute {
705 page: existing_page,
706 ..
707 } => {
708 conflict("route", existing_page);
709 }
710 Entrypoint::AppMetadata {
711 page: existing_page,
712 ..
713 } => {
714 conflict("metadata", existing_page);
715 }
716 }
717}
718
719#[turbo_tasks::function]
720pub fn get_entrypoints(
721 app_dir: FileSystemPath,
722 page_extensions: Vc<Vec<RcStr>>,
723 is_global_not_found_enabled: Vc<bool>,
724) -> Vc<Entrypoints> {
725 directory_tree_to_entrypoints(
726 app_dir.clone(),
727 get_directory_tree(app_dir.clone(), page_extensions),
728 get_global_metadata(app_dir, page_extensions),
729 is_global_not_found_enabled,
730 Default::default(),
731 )
732}
733
734#[turbo_tasks::function]
735fn directory_tree_to_entrypoints(
736 app_dir: FileSystemPath,
737 directory_tree: Vc<DirectoryTree>,
738 global_metadata: Vc<GlobalMetadata>,
739 is_global_not_found_enabled: Vc<bool>,
740 root_layouts: Vc<FileSystemPathVec>,
741) -> Vc<Entrypoints> {
742 directory_tree_to_entrypoints_internal(
743 app_dir,
744 global_metadata,
745 is_global_not_found_enabled,
746 rcstr!(""),
747 directory_tree,
748 AppPage::new(),
749 root_layouts,
750 )
751}
752
753#[turbo_tasks::value]
754struct DuplicateParallelRouteIssue {
755 app_dir: FileSystemPath,
756 previously_inserted_page: AppPage,
757 page: AppPage,
758}
759
760#[turbo_tasks::value_impl]
761impl Issue for DuplicateParallelRouteIssue {
762 #[turbo_tasks::function]
763 fn file_path(&self) -> Result<Vc<FileSystemPath>> {
764 Ok(self.app_dir.join(&self.page.to_string())?.cell())
765 }
766
767 #[turbo_tasks::function]
768 fn stage(self: Vc<Self>) -> Vc<IssueStage> {
769 IssueStage::ProcessModule.cell()
770 }
771
772 #[turbo_tasks::function]
773 async fn title(self: Vc<Self>) -> Result<Vc<StyledString>> {
774 let this = self.await?;
775 Ok(StyledString::Text(
776 format!(
777 "You cannot have two parallel pages that resolve to the same path. Please check \
778 {} and {}.",
779 this.previously_inserted_page, this.page
780 )
781 .into(),
782 )
783 .cell())
784 }
785}
786
787fn page_path_except_parallel(loader_tree: &AppPageLoaderTree) -> Option<AppPage> {
788 if loader_tree.page.iter().any(|v| {
789 matches!(
790 v,
791 PageSegment::CatchAll(..)
792 | PageSegment::OptionalCatchAll(..)
793 | PageSegment::Parallel(..)
794 )
795 }) {
796 return None;
797 }
798
799 if loader_tree.modules.page.is_some() {
800 return Some(loader_tree.page.clone());
801 }
802
803 if let Some(children) = loader_tree.parallel_routes.get("children") {
804 return page_path_except_parallel(children);
805 }
806
807 None
808}
809
810async fn check_duplicate(
811 duplicate: &mut FxHashMap<AppPath, AppPage>,
812 loader_tree: &AppPageLoaderTree,
813 app_dir: FileSystemPath,
814) -> Result<()> {
815 let page_path = page_path_except_parallel(loader_tree);
816
817 if let Some(page_path) = page_path
818 && let Some(prev) = duplicate.insert(AppPath::from(page_path.clone()), page_path.clone())
819 && prev != page_path
820 {
821 DuplicateParallelRouteIssue {
822 app_dir: app_dir.clone(),
823 previously_inserted_page: prev.clone(),
824 page: loader_tree.page.clone(),
825 }
826 .resolved_cell()
827 .emit();
828 }
829
830 Ok(())
831}
832
833#[turbo_tasks::value(transparent)]
834struct AppPageLoaderTreeOption(Option<ResolvedVc<AppPageLoaderTree>>);
835
836#[turbo_tasks::function]
838async fn directory_tree_to_loader_tree(
839 app_dir: FileSystemPath,
840 global_metadata: Vc<GlobalMetadata>,
841 directory_name: RcStr,
842 directory_tree: Vc<DirectoryTree>,
843 app_page: AppPage,
844 for_app_path: AppPath,
846) -> Result<Vc<AppPageLoaderTreeOption>> {
847 let plain_tree = &*directory_tree.into_plain().await?;
848
849 let tree = directory_tree_to_loader_tree_internal(
850 app_dir,
851 global_metadata,
852 directory_name,
853 plain_tree,
854 app_page,
855 for_app_path,
856 )
857 .await?;
858
859 Ok(Vc::cell(tree.map(AppPageLoaderTree::resolved_cell)))
860}
861
862async fn directory_tree_to_loader_tree_internal(
863 app_dir: FileSystemPath,
864 global_metadata: Vc<GlobalMetadata>,
865 directory_name: RcStr,
866 directory_tree: &PlainDirectoryTree,
867 app_page: AppPage,
868 for_app_path: AppPath,
870) -> Result<Option<AppPageLoaderTree>> {
871 let app_path = AppPath::from(app_page.clone());
872
873 if !for_app_path.contains(&app_path) {
874 return Ok(None);
875 }
876
877 let mut modules = directory_tree.modules.clone();
878
879 modules.metadata.base_page = Some(app_page.clone());
882
883 let is_root_directory = app_page.is_root();
885 let is_root_layout = app_path.is_root() && modules.layout.is_some();
888
889 if is_root_directory || is_root_layout {
890 if modules.not_found.is_none() {
891 modules.not_found = Some(
892 get_next_package(app_dir.clone())
893 .await?
894 .join("dist/client/components/builtin/not-found.js")?,
895 );
896 }
897 if modules.forbidden.is_none() {
898 modules.forbidden = Some(
899 get_next_package(app_dir.clone())
900 .await?
901 .join("dist/client/components/builtin/forbidden.js")?,
902 );
903 }
904 if modules.unauthorized.is_none() {
905 modules.unauthorized = Some(
906 get_next_package(app_dir.clone())
907 .await?
908 .join("dist/client/components/builtin/unauthorized.js")?,
909 );
910 }
911 if modules.global_error.is_none() {
912 modules.global_error = Some(
913 get_next_package(app_dir.clone())
914 .await?
915 .join("dist/client/components/builtin/global-error.js")?,
916 );
917 }
918 }
919
920 let mut tree = AppPageLoaderTree {
921 page: app_page.clone(),
922 segment: directory_name.clone(),
923 parallel_routes: FxIndexMap::default(),
924 modules: modules.without_leaves(),
925 global_metadata: global_metadata.to_resolved().await?,
926 };
927
928 let current_level_is_parallel_route = is_parallel_route(&directory_name);
929
930 if current_level_is_parallel_route {
931 tree.segment = rcstr!("children");
932 }
933
934 if let Some(page) = (app_path == for_app_path || app_path.is_catchall())
935 .then_some(modules.page)
936 .flatten()
937 {
938 tree.parallel_routes.insert(
939 rcstr!("children"),
940 AppPageLoaderTree {
941 page: app_page.clone(),
942 segment: rcstr!("__PAGE__"),
943 parallel_routes: FxIndexMap::default(),
944 modules: AppDirModules {
945 page: Some(page),
946 metadata: modules.metadata,
947 ..Default::default()
948 },
949 global_metadata: global_metadata.to_resolved().await?,
950 },
951 );
952
953 if current_level_is_parallel_route {
954 tree.segment = rcstr!("page$");
955 }
956 }
957
958 let mut duplicate = FxHashMap::default();
959
960 for (subdir_name, subdirectory) in &directory_tree.subdirectories {
961 let parallel_route_key = match_parallel_route(subdir_name);
962
963 let mut child_app_page = app_page.clone();
964 let mut illegal_path_error = None;
965
966 if let Err(e) = child_app_page.push_str(&normalize_underscore(subdir_name)) {
970 illegal_path_error = Some(e);
971 }
972
973 let subtree = Box::pin(directory_tree_to_loader_tree_internal(
974 app_dir.clone(),
975 global_metadata,
976 subdir_name.clone(),
977 subdirectory,
978 child_app_page.clone(),
979 for_app_path.clone(),
980 ))
981 .await?;
982
983 if let Some(illegal_path) = subtree.as_ref().and(illegal_path_error) {
984 return Err(illegal_path);
985 }
986
987 if let Some(subtree) = subtree {
988 if let Some(key) = parallel_route_key {
989 tree.parallel_routes.insert(key.into(), subtree);
990 continue;
991 }
992
993 if is_group_route(subdir_name) && !subtree.has_page() {
995 continue;
996 }
997
998 if subtree.has_page() {
999 check_duplicate(&mut duplicate, &subtree, app_dir.clone()).await?;
1000 }
1001
1002 if let Some(current_tree) = tree.parallel_routes.get("children") {
1003 if current_tree.has_only_catchall()
1004 && (!subtree.has_only_catchall()
1005 || current_tree.get_specificity() < subtree.get_specificity())
1006 {
1007 tree.parallel_routes
1008 .insert(rcstr!("children"), subtree.clone());
1009 }
1010 } else {
1011 tree.parallel_routes.insert(rcstr!("children"), subtree);
1012 }
1013 } else if let Some(key) = parallel_route_key {
1014 bail!(
1015 "missing page or default for parallel route `{}` (page: {})",
1016 key,
1017 app_page
1018 );
1019 }
1020 }
1021
1022 if tree
1025 .parallel_routes
1026 .iter()
1027 .any(|(_, parallel_tree)| parallel_tree.is_intercepting())
1028 {
1029 let mut keys_to_replace = Vec::new();
1030
1031 for (key, parallel_tree) in &tree.parallel_routes {
1032 if !parallel_tree.is_intercepting() {
1033 keys_to_replace.push(key.clone());
1034 }
1035 }
1036
1037 for key in keys_to_replace {
1038 let subdir_name: RcStr = format!("@{key}").into();
1039
1040 let default = if key == "children" {
1041 modules.default.clone()
1042 } else if let Some(subdirectory) = directory_tree.subdirectories.get(&subdir_name) {
1043 subdirectory.modules.default.clone()
1044 } else {
1045 None
1046 };
1047
1048 tree.parallel_routes.insert(
1049 key,
1050 default_route_tree(app_dir.clone(), global_metadata, app_page.clone(), default)
1051 .await?,
1052 );
1053 }
1054 }
1055
1056 if tree.parallel_routes.is_empty() {
1057 if modules.default.is_some() || current_level_is_parallel_route {
1058 tree = default_route_tree(
1059 app_dir.clone(),
1060 global_metadata,
1061 app_page,
1062 modules.default.clone(),
1063 )
1064 .await?;
1065 } else {
1066 return Ok(None);
1067 }
1068 } else if tree.parallel_routes.get("children").is_none() {
1069 tree.parallel_routes.insert(
1070 rcstr!("children"),
1071 default_route_tree(
1072 app_dir.clone(),
1073 global_metadata,
1074 app_page,
1075 modules.default.clone(),
1076 )
1077 .await?,
1078 );
1079 }
1080
1081 if tree.parallel_routes.len() > 1
1082 && tree.parallel_routes.keys().next().map(|s| s.as_str()) != Some("children")
1083 {
1084 tree.parallel_routes
1086 .move_index(tree.parallel_routes.len() - 1, 0);
1087 }
1088
1089 Ok(Some(tree))
1090}
1091
1092async fn default_route_tree(
1093 app_dir: FileSystemPath,
1094 global_metadata: Vc<GlobalMetadata>,
1095 app_page: AppPage,
1096 default_component: Option<FileSystemPath>,
1097) -> Result<AppPageLoaderTree> {
1098 Ok(AppPageLoaderTree {
1099 page: app_page.clone(),
1100 segment: rcstr!("__DEFAULT__"),
1101 parallel_routes: FxIndexMap::default(),
1102 modules: if let Some(default) = default_component {
1103 AppDirModules {
1104 default: Some(default),
1105 ..Default::default()
1106 }
1107 } else {
1108 AppDirModules {
1110 default: Some(
1111 get_next_package(app_dir)
1112 .await?
1113 .join("dist/client/components/builtin/default.js")?,
1114 ),
1115 ..Default::default()
1116 }
1117 },
1118 global_metadata: global_metadata.to_resolved().await?,
1119 })
1120}
1121
1122#[turbo_tasks::function]
1123async fn directory_tree_to_entrypoints_internal(
1124 app_dir: FileSystemPath,
1125 global_metadata: ResolvedVc<GlobalMetadata>,
1126 is_global_not_found_enabled: Vc<bool>,
1127 directory_name: RcStr,
1128 directory_tree: Vc<DirectoryTree>,
1129 app_page: AppPage,
1130 root_layouts: ResolvedVc<FileSystemPathVec>,
1131) -> Result<Vc<Entrypoints>> {
1132 let span = tracing::info_span!("build layout trees", name = display(&app_page));
1133 directory_tree_to_entrypoints_internal_untraced(
1134 app_dir,
1135 global_metadata,
1136 is_global_not_found_enabled,
1137 directory_name,
1138 directory_tree,
1139 app_page,
1140 root_layouts,
1141 )
1142 .instrument(span)
1143 .await
1144}
1145
1146async fn directory_tree_to_entrypoints_internal_untraced(
1147 app_dir: FileSystemPath,
1148 global_metadata: ResolvedVc<GlobalMetadata>,
1149 is_global_not_found_enabled: Vc<bool>,
1150 directory_name: RcStr,
1151 directory_tree: Vc<DirectoryTree>,
1152 app_page: AppPage,
1153 root_layouts: ResolvedVc<FileSystemPathVec>,
1154) -> Result<Vc<Entrypoints>> {
1155 let mut result = FxIndexMap::default();
1156
1157 let directory_tree_vc = directory_tree;
1158 let directory_tree = &*directory_tree.await?;
1159
1160 let subdirectories = &directory_tree.subdirectories;
1161 let modules = &directory_tree.modules;
1162 let root_layouts = if let Some(layout) = &modules.layout {
1166 let mut layouts = root_layouts.owned().await?;
1167 layouts.push(layout.clone());
1168 ResolvedVc::cell(layouts)
1169 } else {
1170 root_layouts
1171 };
1172
1173 if modules.page.is_some() {
1174 let app_path = AppPath::from(app_page.clone());
1175
1176 let loader_tree = *directory_tree_to_loader_tree(
1177 app_dir.clone(),
1178 *global_metadata,
1179 directory_name.clone(),
1180 directory_tree_vc,
1181 app_page.clone(),
1182 app_path,
1183 )
1184 .await?;
1185
1186 add_app_page(
1187 app_dir.clone(),
1188 &mut result,
1189 app_page.complete(PageType::Page)?,
1190 loader_tree.context("loader tree should be created for a page/default")?,
1191 );
1192 }
1193
1194 if let Some(route) = &modules.route {
1195 add_app_route(
1196 app_dir.clone(),
1197 &mut result,
1198 app_page.complete(PageType::Route)?,
1199 route.clone(),
1200 root_layouts,
1201 );
1202 }
1203
1204 let Metadata {
1205 icon,
1206 apple,
1207 twitter,
1208 open_graph,
1209 sitemap,
1210 base_page: _,
1211 } = &modules.metadata;
1212
1213 for meta in sitemap
1214 .iter()
1215 .cloned()
1216 .chain(icon.iter().cloned().map(MetadataItem::from))
1217 .chain(apple.iter().cloned().map(MetadataItem::from))
1218 .chain(twitter.iter().cloned().map(MetadataItem::from))
1219 .chain(open_graph.iter().cloned().map(MetadataItem::from))
1220 {
1221 let app_page = app_page.clone_push_str(&get_metadata_route_name(meta.clone()).await?)?;
1222
1223 add_app_metadata_route(
1224 app_dir.clone(),
1225 &mut result,
1226 normalize_metadata_route(app_page)?,
1227 meta,
1228 );
1229 }
1230
1231 if app_page.is_root() {
1233 let GlobalMetadata {
1234 favicon,
1235 robots,
1236 manifest,
1237 } = &*global_metadata.await?;
1238
1239 for meta in favicon.iter().chain(robots.iter()).chain(manifest.iter()) {
1240 let app_page =
1241 app_page.clone_push_str(&get_metadata_route_name(meta.clone()).await?)?;
1242
1243 add_app_metadata_route(
1244 app_dir.clone(),
1245 &mut result,
1246 normalize_metadata_route(app_page)?,
1247 meta.clone(),
1248 );
1249 }
1250
1251 let mut modules = directory_tree.modules.clone();
1252
1253 if modules.layout.is_none() {
1255 modules.layout = Some(
1256 get_next_package(app_dir.clone())
1257 .await?
1258 .join("dist/client/components/builtin/layout.js")?,
1259 );
1260 }
1261
1262 if modules.not_found.is_none() {
1263 modules.not_found = Some(
1264 get_next_package(app_dir.clone())
1265 .await?
1266 .join("dist/client/components/builtin/not-found.js")?,
1267 );
1268 }
1269 if modules.forbidden.is_none() {
1270 modules.forbidden = Some(
1271 get_next_package(app_dir.clone())
1272 .await?
1273 .join("dist/client/components/builtin/forbidden.js")?,
1274 );
1275 }
1276 if modules.unauthorized.is_none() {
1277 modules.unauthorized = Some(
1278 get_next_package(app_dir.clone())
1279 .await?
1280 .join("dist/client/components/builtin/unauthorized.js")?,
1281 );
1282 }
1283
1284 let is_global_not_found_enabled = *is_global_not_found_enabled.await?;
1289 let use_global_not_found =
1290 is_global_not_found_enabled || modules.global_not_found.is_some();
1291
1292 let not_found_root_modules = modules.without_leaves();
1293 let not_found_tree = AppPageLoaderTree {
1294 page: app_page.clone(),
1295 segment: directory_name.clone(),
1296 parallel_routes: fxindexmap! {
1297 rcstr!("children") => AppPageLoaderTree {
1298 page: app_page.clone(),
1299 segment: rcstr!("/_not-found"),
1300 parallel_routes: fxindexmap! {
1301 rcstr!("children") => AppPageLoaderTree {
1302 page: app_page.clone(),
1303 segment: rcstr!("__PAGE__"),
1304 parallel_routes: FxIndexMap::default(),
1305 modules: if use_global_not_found {
1306 AppDirModules {
1309 layout: None,
1310 page: match modules.global_not_found {
1311 Some(v) => Some(v),
1312 None => Some(get_next_package(app_dir.clone())
1313 .await?
1314 .join("dist/client/components/builtin/global-not-found.js")?,
1315 ),
1316 },
1317 ..Default::default()
1318 }
1319 } else {
1320 AppDirModules {
1323 page: match modules.not_found {
1324 Some(v) => Some(v),
1325 None => Some(get_next_package(app_dir.clone())
1326 .await?
1327 .join("dist/client/components/builtin/not-found.js")?,
1328 ),
1329 },
1330 ..Default::default()
1331 }
1332 },
1333 global_metadata,
1334 }
1335 },
1336 modules: AppDirModules {
1337 ..Default::default()
1338 },
1339 global_metadata,
1340 },
1341 },
1342 modules: AppDirModules {
1343 layout: if use_global_not_found {
1348 None
1349 } else {
1350 modules.layout
1351 },
1352 ..not_found_root_modules
1353 },
1354 global_metadata,
1355 }
1356 .resolved_cell();
1357
1358 {
1359 let app_page = app_page
1360 .clone_push_str("_not-found")?
1361 .complete(PageType::Page)?;
1362
1363 add_app_page(app_dir.clone(), &mut result, app_page, not_found_tree);
1364 }
1365 }
1366
1367 let app_page = &app_page;
1368 let directory_name = &directory_name;
1369 let subdirectories = subdirectories
1370 .iter()
1371 .map(|(subdir_name, &subdirectory)| {
1372 let app_dir = app_dir.clone();
1373
1374 async move {
1375 let mut child_app_page = app_page.clone();
1376 let mut illegal_path = None;
1377
1378 if let Err(e) = child_app_page.push_str(&normalize_underscore(subdir_name)) {
1382 illegal_path = Some(e);
1383 }
1384
1385 let map = directory_tree_to_entrypoints_internal(
1386 app_dir.clone(),
1387 *global_metadata,
1388 is_global_not_found_enabled,
1389 subdir_name.clone(),
1390 *subdirectory,
1391 child_app_page.clone(),
1392 *root_layouts,
1393 )
1394 .await?;
1395
1396 if let Some(illegal_path) = illegal_path
1397 && !map.is_empty()
1398 {
1399 return Err(illegal_path);
1400 }
1401
1402 let mut loader_trees = Vec::new();
1403
1404 for (_, entrypoint) in map.iter() {
1405 if let Entrypoint::AppPage {
1406 ref pages,
1407 loader_tree: _,
1408 } = *entrypoint
1409 {
1410 for page in pages {
1411 let app_path = AppPath::from(page.clone());
1412
1413 let loader_tree = directory_tree_to_loader_tree(
1414 app_dir.clone(),
1415 *global_metadata,
1416 directory_name.clone(),
1417 directory_tree_vc,
1418 app_page.clone(),
1419 app_path,
1420 );
1421 loader_trees.push(loader_tree);
1422 }
1423 }
1424 }
1425 Ok((map, loader_trees))
1426 }
1427 })
1428 .try_join()
1429 .await?;
1430
1431 for (map, loader_trees) in subdirectories.iter() {
1432 let mut i = 0;
1433 for (_, entrypoint) in map.iter() {
1434 match entrypoint {
1435 Entrypoint::AppPage {
1436 pages,
1437 loader_tree: _,
1438 } => {
1439 for page in pages {
1440 let loader_tree = *loader_trees[i].await?;
1441 i += 1;
1442
1443 add_app_page(
1444 app_dir.clone(),
1445 &mut result,
1446 page.clone(),
1447 loader_tree
1448 .context("loader tree should be created for a page/default")?,
1449 );
1450 }
1451 }
1452 Entrypoint::AppRoute {
1453 page,
1454 path,
1455 root_layouts,
1456 } => {
1457 add_app_route(
1458 app_dir.clone(),
1459 &mut result,
1460 page.clone(),
1461 path.clone(),
1462 *root_layouts,
1463 );
1464 }
1465 Entrypoint::AppMetadata { page, metadata } => {
1466 add_app_metadata_route(
1467 app_dir.clone(),
1468 &mut result,
1469 page.clone(),
1470 metadata.clone(),
1471 );
1472 }
1473 }
1474 }
1475 }
1476 Ok(Vc::cell(result))
1477}
1478
1479#[turbo_tasks::function]
1481pub async fn get_global_metadata(
1482 app_dir: FileSystemPath,
1483 page_extensions: Vc<Vec<RcStr>>,
1484) -> Result<Vc<GlobalMetadata>> {
1485 let DirectoryContent::Entries(entries) = &*app_dir.read_dir().await? else {
1486 bail!("app_dir must be a directory")
1487 };
1488 let mut metadata = GlobalMetadata::default();
1489
1490 for (basename, entry) in entries {
1491 let DirectoryEntry::File(file) = entry else {
1492 continue;
1493 };
1494
1495 let Some(GlobalMetadataFileMatch {
1496 metadata_type,
1497 dynamic,
1498 }) = match_global_metadata_file(basename, &page_extensions.await?)
1499 else {
1500 continue;
1501 };
1502
1503 let entry = match metadata_type {
1504 "favicon" => &mut metadata.favicon,
1505 "manifest" => &mut metadata.manifest,
1506 "robots" => &mut metadata.robots,
1507 _ => continue,
1508 };
1509
1510 if dynamic {
1511 *entry = Some(MetadataItem::Dynamic { path: file.clone() });
1512 } else {
1513 *entry = Some(MetadataItem::Static { path: file.clone() });
1514 }
1515 }
1517
1518 Ok(metadata.cell())
1519}
1520
1521#[turbo_tasks::value(shared)]
1522struct DirectoryTreeIssue {
1523 pub severity: IssueSeverity,
1524 pub app_dir: FileSystemPath,
1525 pub message: ResolvedVc<StyledString>,
1526}
1527
1528#[turbo_tasks::value_impl]
1529impl Issue for DirectoryTreeIssue {
1530 fn severity(&self) -> IssueSeverity {
1531 self.severity
1532 }
1533
1534 #[turbo_tasks::function]
1535 fn title(&self) -> Vc<StyledString> {
1536 StyledString::Text(rcstr!("An issue occurred while preparing your Next.js app")).cell()
1537 }
1538
1539 #[turbo_tasks::function]
1540 fn stage(&self) -> Vc<IssueStage> {
1541 IssueStage::AppStructure.cell()
1542 }
1543
1544 #[turbo_tasks::function]
1545 fn file_path(&self) -> Vc<FileSystemPath> {
1546 self.app_dir.clone().cell()
1547 }
1548
1549 #[turbo_tasks::function]
1550 fn description(&self) -> Vc<OptionStyledString> {
1551 Vc::cell(Some(self.message))
1552 }
1553}