1use std::collections::BTreeMap;
2
3use anyhow::{Context, Result, bail};
4use async_trait::async_trait;
5use bincode::{Decode, Encode};
6use indexmap::map::{Entry, OccupiedEntry};
7use rustc_hash::FxHashMap;
8use tracing::Instrument;
9use turbo_rcstr::{RcStr, rcstr};
10use turbo_tasks::{
11 FxIndexMap, FxIndexSet, NonLocalValue, ResolvedVc, TryJoinIterExt, ValueDefault,
12 ValueToStringRef, Vc, debug::ValueDebugFormat, fxindexmap, turbobail,
13};
14use turbo_tasks_fs::{DirectoryContent, DirectoryEntry, FileSystemEntryType, FileSystemPath};
15use turbopack_core::issue::{Issue, IssueExt, IssueSeverity, IssueStage, StyledString};
16
17use crate::{
18 mode::NextMode,
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 pub page: Option<FileSystemPath>,
40 pub layout: Option<FileSystemPath>,
41 pub error: Option<FileSystemPath>,
42 pub global_error: Option<FileSystemPath>,
43 pub global_not_found: Option<FileSystemPath>,
44 pub loading: Option<FileSystemPath>,
45 pub template: Option<FileSystemPath>,
46 pub forbidden: Option<FileSystemPath>,
47 pub unauthorized: Option<FileSystemPath>,
48 pub not_found: Option<FileSystemPath>,
49 pub default: Option<FileSystemPath>,
50 pub route: Option<FileSystemPath>,
51 pub metadata: Metadata,
52}
53
54impl AppDirModules {
55 fn without_leaves(&self) -> Self {
56 Self {
57 page: None,
58 layout: self.layout.clone(),
59 error: self.error.clone(),
60 global_error: self.global_error.clone(),
61 global_not_found: self.global_not_found.clone(),
62 loading: self.loading.clone(),
63 template: self.template.clone(),
64 not_found: self.not_found.clone(),
65 forbidden: self.forbidden.clone(),
66 unauthorized: self.unauthorized.clone(),
67 default: None,
68 route: None,
69 metadata: self.metadata.clone(),
70 }
71 }
72}
73
74#[derive(Clone, Debug, PartialEq, Eq, NonLocalValue, Encode, Decode)]
76pub enum MetadataWithAltItem {
77 Static {
78 path: FileSystemPath,
79 alt_path: Option<FileSystemPath>,
80 },
81 Dynamic {
82 path: FileSystemPath,
83 },
84}
85
86#[turbo_tasks::task_input]
88#[derive(Clone, Debug, Hash, PartialEq, Eq, Encode, Decode)]
89pub enum MetadataItem {
90 Static { path: FileSystemPath },
91 Dynamic { path: FileSystemPath },
92}
93
94#[turbo_tasks::function]
95pub async fn get_metadata_route_name(meta: MetadataItem) -> Result<Vc<RcStr>> {
96 Ok(match meta {
97 MetadataItem::Static { path } => Vc::cell(path.file_name().into()),
98 MetadataItem::Dynamic { path } => {
99 let Some(stem) = path.file_stem() else {
100 turbobail!("unable to resolve file stem for metadata item at {path}");
101 };
102
103 match stem {
104 "manifest" => Vc::cell(rcstr!("manifest.webmanifest")),
105 _ => Vc::cell(RcStr::from(stem)),
106 }
107 }
108 })
109}
110
111impl MetadataItem {
112 pub fn into_path(self) -> FileSystemPath {
113 match self {
114 MetadataItem::Static { path } => path,
115 MetadataItem::Dynamic { path } => path,
116 }
117 }
118}
119
120impl From<MetadataWithAltItem> for MetadataItem {
121 fn from(value: MetadataWithAltItem) -> Self {
122 match value {
123 MetadataWithAltItem::Static { path, .. } => MetadataItem::Static { path },
124 MetadataWithAltItem::Dynamic { path } => MetadataItem::Dynamic { path },
125 }
126 }
127}
128
129#[derive(Default, Clone, Debug, PartialEq, Eq, NonLocalValue, Encode, Decode)]
131pub struct Metadata {
132 pub icon: Vec<MetadataWithAltItem>,
133 pub apple: Vec<MetadataWithAltItem>,
134 pub twitter: Vec<MetadataWithAltItem>,
135 pub open_graph: Vec<MetadataWithAltItem>,
136 pub sitemap: Option<MetadataItem>,
137 pub base_page: Option<AppPage>,
145}
146
147impl Metadata {
148 pub fn is_empty(&self) -> bool {
149 let Metadata {
150 icon,
151 apple,
152 twitter,
153 open_graph,
154 sitemap,
155 base_page: _,
156 } = self;
157 icon.is_empty()
158 && apple.is_empty()
159 && twitter.is_empty()
160 && open_graph.is_empty()
161 && sitemap.is_none()
162 }
163}
164
165#[turbo_tasks::value]
167#[derive(Default, Clone, Debug)]
168pub struct GlobalMetadata {
169 pub favicon: Option<MetadataItem>,
170 pub robots: Option<MetadataItem>,
171 pub manifest: Option<MetadataItem>,
172}
173
174impl GlobalMetadata {
175 pub fn is_empty(&self) -> bool {
176 let GlobalMetadata {
177 favicon,
178 robots,
179 manifest,
180 } = self;
181 favicon.is_none() && robots.is_none() && manifest.is_none()
182 }
183}
184
185#[turbo_tasks::value]
186#[derive(Debug)]
187pub struct DirectoryTree {
188 pub subdirectories: BTreeMap<RcStr, ResolvedVc<DirectoryTree>>,
190 pub modules: AppDirModules,
191}
192
193#[turbo_tasks::value]
194#[derive(Clone, Debug)]
195struct PlainDirectoryTree {
196 pub subdirectories: BTreeMap<RcStr, PlainDirectoryTree>,
198 pub modules: AppDirModules,
199 pub contains_page_or_default: bool,
201 pub url_tree: UrlSegmentTree,
203}
204
205#[derive(Clone, Debug, Default, PartialEq, Eq, NonLocalValue, Encode, Decode)]
232struct UrlSegmentTree {
233 pub children: BTreeMap<RcStr, UrlSegmentTree>,
234}
235
236impl UrlSegmentTree {
237 fn static_children(&self) -> Vec<RcStr> {
238 self.children
239 .keys()
240 .filter(|name| !is_dynamic_segment(name))
241 .cloned()
242 .collect()
243 }
244
245 fn get_child(&self, segment: &str) -> Option<&UrlSegmentTree> {
246 self.children.get(segment)
247 }
248}
249
250fn build_url_segment_tree_from_subdirs(
251 subdirs: &BTreeMap<RcStr, PlainDirectoryTree>,
252) -> UrlSegmentTree {
253 let mut result = UrlSegmentTree::default();
254 build_url_segment_tree_recursive(subdirs, &mut result);
255 result
256}
257
258fn build_url_segment_tree_recursive(
265 subdirs: &BTreeMap<RcStr, PlainDirectoryTree>,
266 result: &mut UrlSegmentTree,
267) {
268 for (name, subtree) in subdirs {
269 if is_url_transparent_segment(name) {
270 build_url_segment_tree_recursive(&subtree.subdirectories, result);
274 } else {
275 let child = result.children.entry(name.clone()).or_default();
280 build_url_segment_tree_recursive(&subtree.subdirectories, child);
281 }
282 }
283}
284
285#[turbo_tasks::value_impl]
286impl DirectoryTree {
287 #[turbo_tasks::function]
288 pub async fn into_plain(&self) -> Result<Vc<PlainDirectoryTree>> {
289 let mut subdirectories = BTreeMap::new();
290
291 for (name, subdirectory) in &self.subdirectories {
292 subdirectories.insert(name.clone(), subdirectory.into_plain().owned().await?);
293 }
294
295 let contains_page_or_default = self.modules.page.is_some()
296 || self.modules.default.is_some()
297 || subdirectories
298 .values()
299 .any(|subdirectory| subdirectory.contains_page_or_default);
300 let url_tree = build_url_segment_tree_from_subdirs(&subdirectories);
301
302 Ok(PlainDirectoryTree {
303 subdirectories,
304 modules: self.modules.clone(),
305 contains_page_or_default,
306 url_tree,
307 }
308 .cell())
309 }
310}
311
312#[turbo_tasks::value(transparent)]
313pub struct OptionAppDir(Option<FileSystemPath>);
314
315#[turbo_tasks::function]
317pub async fn find_app_dir(project_path: FileSystemPath) -> Result<Vc<OptionAppDir>> {
318 let app = project_path.join("app")?;
319 let src_app = project_path.join("src/app")?;
320 let app_dir = if *app.get_type().await? == FileSystemEntryType::Directory {
321 app
322 } else if *src_app.get_type().await? == FileSystemEntryType::Directory {
323 src_app
324 } else {
325 return Ok(Vc::cell(None));
326 };
327
328 Ok(Vc::cell(Some(app_dir)))
329}
330
331#[turbo_tasks::function]
332async fn get_directory_tree(
333 dir: FileSystemPath,
334 page_extensions: Vc<Vec<RcStr>>,
335) -> Result<Vc<DirectoryTree>> {
336 let span = tracing::info_span!(
337 "read app directory tree",
338 name = display(dir.to_string_ref().await?)
339 );
340 get_directory_tree_internal(dir, page_extensions)
341 .instrument(span)
342 .await
343}
344
345async fn get_directory_tree_internal(
346 dir: FileSystemPath,
347 page_extensions: Vc<Vec<RcStr>>,
348) -> Result<Vc<DirectoryTree>> {
349 let DirectoryContent::Entries(entries) = &*dir.read_dir().await? else {
350 return Ok(DirectoryTree {
354 subdirectories: Default::default(),
355 modules: AppDirModules::default(),
356 }
357 .cell());
358 };
359 let page_extensions_value = page_extensions.await?;
360
361 let mut subdirectories = BTreeMap::new();
362 let mut modules = AppDirModules::default();
363
364 let mut metadata_icon = Vec::new();
365 let mut metadata_apple = Vec::new();
366 let mut metadata_open_graph = Vec::new();
367 let mut metadata_twitter = Vec::new();
368
369 for (basename, entry) in entries {
370 let entry = entry.clone().resolve_symlink().await?;
371 match entry {
372 DirectoryEntry::File(file) => {
373 if basename.ends_with(".d.ts") {
375 continue;
376 }
377 if let Some((stem, ext)) = basename.split_once('.')
378 && page_extensions_value.iter().any(|e| e == ext)
379 {
380 match stem {
381 "page" => modules.page = Some(file.clone()),
382 "layout" => modules.layout = Some(file.clone()),
383 "error" => modules.error = Some(file.clone()),
384 "global-error" => modules.global_error = Some(file.clone()),
385 "global-not-found" => modules.global_not_found = Some(file.clone()),
386 "loading" => modules.loading = Some(file.clone()),
387 "template" => modules.template = Some(file.clone()),
388 "forbidden" => modules.forbidden = Some(file.clone()),
389 "unauthorized" => modules.unauthorized = Some(file.clone()),
390 "not-found" => modules.not_found = Some(file.clone()),
391 "default" => modules.default = Some(file.clone()),
392 "route" => modules.route = Some(file.clone()),
393 _ => {}
394 }
395 }
396
397 let Some(MetadataFileMatch {
398 metadata_type,
399 number,
400 dynamic,
401 }) = match_local_metadata_file(basename.as_str(), &page_extensions_value)
402 else {
403 continue;
404 };
405
406 let entry = match metadata_type {
407 "icon" => &mut metadata_icon,
408 "apple-icon" => &mut metadata_apple,
409 "twitter-image" => &mut metadata_twitter,
410 "opengraph-image" => &mut metadata_open_graph,
411 "sitemap" => {
412 if dynamic {
413 modules.metadata.sitemap = Some(MetadataItem::Dynamic { path: file });
414 } else {
415 modules.metadata.sitemap = Some(MetadataItem::Static { path: file });
416 }
417 continue;
418 }
419 _ => continue,
420 };
421
422 if dynamic {
423 entry.push((number, MetadataWithAltItem::Dynamic { path: file }));
424 continue;
425 }
426
427 let file_name = file.file_name();
428 let basename = file_name
429 .rsplit_once('.')
430 .map_or(file_name, |(basename, _)| basename);
431 let alt_path = file.parent().join(&format!("{basename}.alt.txt"))?;
432 let alt_path = matches!(&*alt_path.get_type().await?, FileSystemEntryType::File)
433 .then_some(alt_path);
434
435 entry.push((
436 number,
437 MetadataWithAltItem::Static {
438 path: file,
439 alt_path,
440 },
441 ));
442 }
443 DirectoryEntry::Directory(dir)
444 if !basename.starts_with('_') => {
446 let result = get_directory_tree(dir.clone(), page_extensions)
447 .to_resolved()
448 .await?;
449 subdirectories.insert(basename.clone(), result);
450 }
451 _ => {}
453 }
454 }
455
456 fn sort<T>(mut list: Vec<(Option<u32>, T)>) -> Vec<T> {
457 list.sort_by_key(|(num, _)| *num);
458 list.into_iter().map(|(_, item)| item).collect()
459 }
460
461 modules.metadata.icon = sort(metadata_icon);
462 modules.metadata.apple = sort(metadata_apple);
463 modules.metadata.twitter = sort(metadata_twitter);
464 modules.metadata.open_graph = sort(metadata_open_graph);
465
466 Ok(DirectoryTree {
467 subdirectories,
468 modules,
469 }
470 .cell())
471}
472
473#[turbo_tasks::value]
474#[derive(Debug, Clone)]
475pub struct AppPageLoaderTree {
476 pub page: AppPage,
477 pub segment: RcStr,
478 #[bincode(with = "turbo_bincode::indexmap")]
479 pub parallel_routes: FxIndexMap<RcStr, AppPageLoaderTree>,
480 pub modules: AppDirModules,
481 pub global_metadata: ResolvedVc<GlobalMetadata>,
482 pub static_siblings: Vec<RcStr>,
486}
487
488impl AppPageLoaderTree {
489 fn collect_page_files(&self, pages: &mut FxIndexSet<FileSystemPath>) {
490 if let Some(page) = &self.modules.page {
491 pages.insert(page.clone());
492 }
493
494 for tree in self.parallel_routes.values() {
495 tree.collect_page_files(pages);
496 }
497 }
498
499 pub fn has_page(&self) -> bool {
501 if &*self.segment == "__PAGE__" {
502 return true;
503 }
504
505 for (_, tree) in &self.parallel_routes {
506 if tree.has_page() {
507 return true;
508 }
509 }
510
511 false
512 }
513
514 pub fn has_only_catchall(&self) -> bool {
517 if &*self.segment == "__PAGE__" && !self.page.is_catchall() {
518 return false;
519 }
520
521 for (_, tree) in &self.parallel_routes {
522 if !tree.has_only_catchall() {
523 return false;
524 }
525 }
526
527 true
528 }
529
530 fn contains_catchall_page(&self) -> bool {
531 (&*self.segment == "__PAGE__" && self.page.is_catchall())
532 || self
533 .parallel_routes
534 .values()
535 .any(AppPageLoaderTree::contains_catchall_page)
536 }
537
538 fn is_builtin_not_found_default(&self, builtin_default: &FileSystemPath) -> bool {
539 &*self.segment == "__DEFAULT__"
540 && self.modules.default.as_ref().is_some_and(|default| {
541 default.fs == builtin_default.fs && default.path == builtin_default.path
542 })
543 }
544
545 fn contains_declared_builtin_not_found_default(
549 &self,
550 builtin_default: &FileSystemPath,
551 declared_slots: &FxIndexMap<FileSystemPath, FxIndexSet<RcStr>>,
552 parent_layout: Option<&FileSystemPath>,
553 ) -> bool {
554 let owner_layout = self.modules.layout.as_ref().or(parent_layout);
555
556 self.parallel_routes.iter().any(|(slot, tree)| {
557 let is_declared_default = tree.is_builtin_not_found_default(builtin_default)
558 && owner_layout.is_some_and(|layout| {
559 declared_slots
560 .get(layout)
561 .is_some_and(|slots| slots.contains(slot))
562 });
563
564 is_declared_default
565 || tree.contains_declared_builtin_not_found_default(
566 builtin_default,
567 declared_slots,
568 owner_layout,
569 )
570 })
571 }
572
573 fn collect_builtin_not_found_defaults(
574 &self,
575 builtin_default: &FileSystemPath,
576 declared_slots: &FxIndexMap<FileSystemPath, FxIndexSet<RcStr>>,
577 parent_layout: Option<&FileSystemPath>,
578 missing_slots: &mut FxIndexMap<FileSystemPath, FxIndexSet<RcStr>>,
579 ) {
580 let owner_layout = self.modules.layout.as_ref().or(parent_layout);
581
582 for (slot, tree) in &self.parallel_routes {
583 if tree.is_builtin_not_found_default(builtin_default) {
584 if let Some(owner_layout) = owner_layout
585 && declared_slots
586 .get(owner_layout)
587 .is_some_and(|slots| slots.contains(slot))
588 {
589 missing_slots
590 .entry(owner_layout.clone())
591 .or_default()
592 .insert(slot.clone());
593 }
594 } else {
595 tree.collect_builtin_not_found_defaults(
596 builtin_default,
597 declared_slots,
598 owner_layout,
599 missing_slots,
600 );
601 }
602 }
603 }
604
605 fn has_unmatched_parallel_route(
608 &self,
609 builtin_default: &FileSystemPath,
610 declared_slots: &FxIndexMap<FileSystemPath, FxIndexSet<RcStr>>,
611 parent_layout: Option<&FileSystemPath>,
612 ) -> bool {
613 let owner_layout = self.modules.layout.as_ref().or(parent_layout);
614 let slots_at_level = owner_layout.and_then(|layout| declared_slots.get(layout));
615
616 self.parallel_routes.iter().any(|(catchall_key, tree)| {
617 slots_at_level.is_some_and(|slots| slots.contains(catchall_key))
618 && tree.contains_catchall_page()
619 && self.parallel_routes.iter().any(|(default_key, tree)| {
620 default_key != catchall_key
621 && slots_at_level.is_some_and(|slots| slots.contains(default_key))
622 && tree.is_builtin_not_found_default(builtin_default)
623 })
624 }) || self.parallel_routes.values().any(|tree| {
625 tree.has_unmatched_parallel_route(builtin_default, declared_slots, owner_layout)
626 })
627 }
628
629 pub fn is_intercepting(&self) -> bool {
631 if self.page.is_intercepting() && self.has_page() {
632 return true;
633 }
634
635 for (_, tree) in &self.parallel_routes {
636 if tree.is_intercepting() {
637 return true;
638 }
639 }
640
641 false
642 }
643
644 pub fn get_specificity(&self) -> usize {
647 if &*self.segment == "__PAGE__" {
648 return AppPath::from(self.page.clone()).len();
649 }
650
651 let mut specificity = 0;
652
653 for (_, tree) in &self.parallel_routes {
654 specificity = specificity.max(tree.get_specificity());
655 }
656
657 specificity
658 }
659}
660
661#[turbo_tasks::value(transparent)]
662#[derive(Default)]
663pub struct RootParamVecOption(Option<Vec<RcStr>>);
664
665#[turbo_tasks::value_impl]
666impl ValueDefault for RootParamVecOption {
667 #[turbo_tasks::function]
668 fn value_default() -> Vc<Self> {
669 Vc::cell(Default::default())
670 }
671}
672
673#[turbo_tasks::value(transparent)]
674pub struct FileSystemPathVec(Vec<FileSystemPath>);
675
676#[turbo_tasks::value_impl]
677impl ValueDefault for FileSystemPathVec {
678 #[turbo_tasks::function]
679 fn value_default() -> Vc<Self> {
680 Vc::cell(Vec::new())
681 }
682}
683
684#[turbo_tasks::task_input]
685#[derive(Clone, PartialEq, Eq, Hash, ValueDebugFormat, Debug, Encode, Decode)]
686pub enum Entrypoint {
687 AppPage {
688 pages: Vec<AppPage>,
689 loader_tree: ResolvedVc<AppPageLoaderTree>,
690 participating_page_files: Vec<FileSystemPath>,
691 root_params: ResolvedVc<RootParamVecOption>,
692 },
693 AppRoute {
694 page: AppPage,
695 path: FileSystemPath,
696 root_params: ResolvedVc<RootParamVecOption>,
697 },
698 AppMetadata {
699 page: AppPage,
700 metadata: MetadataItem,
701 root_params: ResolvedVc<RootParamVecOption>,
702 },
703}
704
705impl Entrypoint {
706 pub fn page(&self) -> &AppPage {
707 match self {
708 Entrypoint::AppPage { pages, .. } => pages.first().unwrap(),
709 Entrypoint::AppRoute { page, .. } => page,
710 Entrypoint::AppMetadata { page, .. } => page,
711 }
712 }
713 pub fn root_params(&self) -> ResolvedVc<RootParamVecOption> {
714 match self {
715 Entrypoint::AppPage { root_params, .. } => *root_params,
716 Entrypoint::AppRoute { root_params, .. } => *root_params,
717 Entrypoint::AppMetadata { root_params, .. } => *root_params,
718 }
719 }
720}
721
722#[turbo_tasks::value(transparent)]
723pub struct Entrypoints(
724 #[bincode(with = "turbo_bincode::indexmap")] FxIndexMap<AppPath, Entrypoint>,
725);
726
727fn is_parallel_route(name: &str) -> bool {
728 name.starts_with('@')
729}
730
731fn is_group_route(name: &str) -> bool {
732 name.starts_with('(') && name.ends_with(')')
733}
734
735fn is_url_transparent_segment(name: &str) -> bool {
739 is_group_route(name) || is_parallel_route(name)
740}
741
742fn is_dynamic_segment(name: &str) -> bool {
743 name.starts_with('[') && name.ends_with(']')
744}
745
746fn match_parallel_route(name: &str) -> Option<&str> {
747 name.strip_prefix('@')
748}
749
750fn conflict_issue(
751 app_dir: FileSystemPath,
752 e: &'_ OccupiedEntry<'_, AppPath, Entrypoint>,
753 a: &str,
754 b: &str,
755 value_a: &AppPage,
756 value_b: &AppPage,
757) {
758 let item_names = if a == b {
759 format!("{a}s")
760 } else {
761 format!("{a} and {b}")
762 };
763
764 DirectoryTreeIssue {
765 app_dir,
766 message: StyledString::Text(
767 format!(
768 "Conflicting {} at {}: {a} at {value_a} and {b} at {value_b}",
769 item_names,
770 e.key(),
771 )
772 .into(),
773 )
774 .resolved_cell(),
775 severity: IssueSeverity::Error,
776 }
777 .resolved_cell()
778 .emit();
779}
780
781fn add_app_page(
782 app_dir: FileSystemPath,
783 result: &mut FxIndexMap<AppPath, Entrypoint>,
784 page: AppPage,
785 loader_tree: ResolvedVc<AppPageLoaderTree>,
786 participating_page_files: Vec<FileSystemPath>,
787 root_params: ResolvedVc<RootParamVecOption>,
788) {
789 let mut e = match result.entry(page.clone().into()) {
790 Entry::Occupied(e) => e,
791 Entry::Vacant(e) => {
792 e.insert(Entrypoint::AppPage {
793 pages: vec![page],
794 loader_tree,
795 participating_page_files,
796 root_params,
797 });
798 return;
799 }
800 };
801
802 let conflict = |existing_name: &str, existing_page: &AppPage| {
803 conflict_issue(app_dir, &e, "page", existing_name, &page, existing_page);
804 };
805
806 let value = e.get();
807 match value {
808 Entrypoint::AppPage {
809 pages: existing_pages,
810 loader_tree: existing_loader_tree,
811 ..
812 } => {
813 if *existing_loader_tree != loader_tree {
816 conflict("page", existing_pages.first().unwrap());
817 }
818
819 let Entrypoint::AppPage {
820 pages: stored_pages,
821 participating_page_files: stored_page_files,
822 ..
823 } = e.get_mut()
824 else {
825 unreachable!("Entrypoint::AppPage was already matched");
826 };
827
828 stored_pages.push(page);
829 stored_pages.sort();
830 for page_file in participating_page_files {
831 if !stored_page_files.contains(&page_file) {
832 stored_page_files.push(page_file);
833 }
834 }
835 }
836 Entrypoint::AppRoute {
837 page: existing_page,
838 ..
839 } => {
840 conflict("route", existing_page);
841 }
842 Entrypoint::AppMetadata {
843 page: existing_page,
844 ..
845 } => {
846 conflict("metadata", existing_page);
847 }
848 }
849}
850
851fn add_app_route(
852 app_dir: FileSystemPath,
853 result: &mut FxIndexMap<AppPath, Entrypoint>,
854 page: AppPage,
855 path: FileSystemPath,
856 root_params: ResolvedVc<RootParamVecOption>,
857) {
858 let e = match result.entry(page.clone().into()) {
859 Entry::Occupied(e) => e,
860 Entry::Vacant(e) => {
861 e.insert(Entrypoint::AppRoute {
862 page,
863 path,
864 root_params,
865 });
866 return;
867 }
868 };
869
870 let conflict = |existing_name: &str, existing_page: &AppPage| {
871 conflict_issue(app_dir, &e, "route", existing_name, &page, existing_page);
872 };
873
874 let value = e.get();
875 match value {
876 Entrypoint::AppPage { pages, .. } => {
877 conflict("page", pages.first().unwrap());
878 }
879 Entrypoint::AppRoute {
880 page: existing_page,
881 ..
882 } => {
883 conflict("route", existing_page);
884 }
885 Entrypoint::AppMetadata {
886 page: existing_page,
887 ..
888 } => {
889 conflict("metadata", existing_page);
890 }
891 }
892}
893
894fn add_app_metadata_route(
895 app_dir: FileSystemPath,
896 result: &mut FxIndexMap<AppPath, Entrypoint>,
897 page: AppPage,
898 metadata: MetadataItem,
899 root_params: ResolvedVc<RootParamVecOption>,
900) {
901 let e = match result.entry(page.clone().into()) {
902 Entry::Occupied(e) => e,
903 Entry::Vacant(e) => {
904 e.insert(Entrypoint::AppMetadata {
905 page,
906 metadata,
907 root_params,
908 });
909 return;
910 }
911 };
912
913 let conflict = |existing_name: &str, existing_page: &AppPage| {
914 conflict_issue(app_dir, &e, "metadata", existing_name, &page, existing_page);
915 };
916
917 let value = e.get();
918 match value {
919 Entrypoint::AppPage { pages, .. } => {
920 conflict("page", pages.first().unwrap());
921 }
922 Entrypoint::AppRoute {
923 page: existing_page,
924 ..
925 } => {
926 conflict("route", existing_page);
927 }
928 Entrypoint::AppMetadata {
929 page: existing_page,
930 ..
931 } => {
932 conflict("metadata", existing_page);
933 }
934 }
935}
936
937#[turbo_tasks::function]
938pub fn get_entrypoints(
939 app_dir: FileSystemPath,
940 page_extensions: Vc<Vec<RcStr>>,
941 is_global_not_found_enabled: Vc<bool>,
942 explicit_parallel_route_children: Vc<bool>,
943 strict_route_matching: Vc<bool>,
944 next_mode: Vc<NextMode>,
945) -> Vc<Entrypoints> {
946 directory_tree_to_entrypoints(
947 app_dir.clone(),
948 get_directory_tree(app_dir.clone(), page_extensions),
949 get_global_metadata(app_dir, page_extensions),
950 is_global_not_found_enabled,
951 explicit_parallel_route_children,
952 strict_route_matching,
953 next_mode,
954 Default::default(),
955 Default::default(),
956 )
957}
958
959#[turbo_tasks::value(transparent)]
960pub struct CollectedRootParams(#[bincode(with = "turbo_bincode::indexset")] FxIndexSet<RcStr>);
961
962#[turbo_tasks::function]
963pub async fn collect_root_params(
964 entrypoints: ResolvedVc<Entrypoints>,
965) -> Result<Vc<CollectedRootParams>> {
966 let mut collected_root_params = FxIndexSet::<RcStr>::default();
967 for (_, entrypoint) in entrypoints.await?.iter() {
968 if let Some(ref root_params) = *entrypoint.root_params().await? {
969 collected_root_params.extend(root_params.iter().cloned());
970 }
971 }
972 Ok(Vc::cell(collected_root_params))
973}
974
975#[turbo_tasks::function]
976async fn directory_tree_to_entrypoints(
977 app_dir: FileSystemPath,
978 directory_tree: Vc<DirectoryTree>,
979 global_metadata: Vc<GlobalMetadata>,
980 is_global_not_found_enabled: Vc<bool>,
981 explicit_parallel_route_children: Vc<bool>,
982 strict_route_matching: Vc<bool>,
983 next_mode: Vc<NextMode>,
984 root_layouts: Vc<FileSystemPathVec>,
985 root_params: Vc<RootParamVecOption>,
986) -> Result<Vc<Entrypoints>> {
987 let entrypoints = directory_tree_to_entrypoints_internal(
988 app_dir.clone(),
989 global_metadata,
990 is_global_not_found_enabled,
991 explicit_parallel_route_children,
992 strict_route_matching,
993 next_mode,
994 rcstr!(""),
995 directory_tree,
996 AppPage::new(),
997 root_layouts,
998 root_params,
999 );
1000
1001 if !*strict_route_matching.await? {
1002 return Ok(entrypoints);
1003 }
1004
1005 let builtin_default = get_next_package(app_dir.clone())
1006 .await?
1007 .join("dist/client/components/builtin/default.js")?;
1008 let entrypoints_ref = entrypoints.await?;
1009 let plain_tree = directory_tree.into_plain().await?;
1010 let mut declared_slots = FxIndexMap::default();
1011 collect_declared_parallel_route_slots(&plain_tree, &mut declared_slots);
1012 let mut candidate_entrypoints = FxIndexMap::default();
1013
1014 for (app_path, entrypoint) in entrypoints_ref.iter() {
1020 let is_incomplete = match entrypoint {
1021 Entrypoint::AppPage { loader_tree, .. } => loader_tree
1022 .await?
1023 .has_unmatched_parallel_route(&builtin_default, &declared_slots, None),
1024 _ => false,
1025 };
1026
1027 if !is_incomplete {
1028 candidate_entrypoints.insert(app_path.clone(), entrypoint.clone());
1029 }
1030 }
1031
1032 let mut incompatible_parallel_route_slots = Vec::new();
1033 let mut retained_entrypoints = FxIndexMap::default();
1034
1035 for (app_path, entrypoint) in &candidate_entrypoints {
1039 let Entrypoint::AppPage { loader_tree, .. } = entrypoint else {
1040 retained_entrypoints.insert(app_path.clone(), entrypoint.clone());
1041 continue;
1042 };
1043 let loader_tree = loader_tree.await?;
1044 if !loader_tree.contains_declared_builtin_not_found_default(
1045 &builtin_default,
1046 &declared_slots,
1047 None,
1048 ) {
1049 retained_entrypoints.insert(app_path.clone(), entrypoint.clone());
1050 continue;
1051 }
1052 if app_path.intercepted_path().is_some() {
1053 continue;
1054 }
1055
1056 let mut missing_slots = FxIndexMap::default();
1057 loader_tree.collect_builtin_not_found_defaults(
1058 &builtin_default,
1059 &declared_slots,
1060 None,
1061 &mut missing_slots,
1062 );
1063 if missing_slots.is_empty() {
1064 bail!(
1065 "Invariant: strict route matching retained the incomplete route matcher \
1066 `{app_path}`"
1067 );
1068 }
1069 incompatible_parallel_route_slots.extend(
1070 missing_slots
1071 .into_iter()
1072 .map(|(layout, slots)| (layout, app_path.clone(), slots.into_iter().collect())),
1073 );
1074 }
1075
1076 if !incompatible_parallel_route_slots.is_empty() {
1077 IncompatibleParallelRouteSlotsIssue {
1078 app_dir: app_dir.clone(),
1079 routes: incompatible_parallel_route_slots,
1080 }
1081 .resolved_cell()
1082 .emit();
1083 }
1084
1085 for (app_path, entrypoint) in &retained_entrypoints {
1088 let Entrypoint::AppPage { loader_tree, .. } = entrypoint else {
1089 continue;
1090 };
1091 if !app_path.contains_interception()
1092 && loader_tree
1093 .await?
1094 .contains_declared_builtin_not_found_default(
1095 &builtin_default,
1096 &declared_slots,
1097 None,
1098 )
1099 {
1100 bail!(
1101 "Invariant: strict route matching retained the incomplete route matcher \
1102 `{app_path}`"
1103 );
1104 }
1105 }
1106
1107 let ordinary_routes = retained_entrypoints
1108 .iter()
1109 .filter_map(|(route, entrypoint)| {
1110 (!route.contains_interception() && matches!(entrypoint, Entrypoint::AppPage { .. }))
1111 .then_some(route)
1112 })
1113 .collect::<Vec<_>>();
1114 let missing_canonical_interception_routes = retained_entrypoints
1115 .iter()
1116 .filter_map(|(interception_route, entrypoint)| {
1117 let Entrypoint::AppPage {
1118 participating_page_files,
1119 ..
1120 } = entrypoint
1121 else {
1122 return None;
1123 };
1124 let canonical_route = interception_route.intercepted_path()?;
1125 if canonical_route.is_route_pattern_covered_by(ordinary_routes.iter().copied()) {
1126 return None;
1127 }
1128
1129 Some((
1130 interception_route.clone(),
1131 canonical_route,
1132 participating_page_files.first()?.clone(),
1133 ))
1134 })
1135 .collect::<Vec<_>>();
1136 if !missing_canonical_interception_routes.is_empty() {
1137 MissingCanonicalInterceptionRoutesIssue {
1138 routes: missing_canonical_interception_routes,
1139 }
1140 .resolved_cell()
1141 .emit();
1142 }
1143
1144 let mut authored_pages = FxIndexSet::default();
1145 collect_authored_page_files(&plain_tree, &mut authored_pages);
1146
1147 let mut matched_pages = FxIndexSet::default();
1148 for entrypoint in retained_entrypoints.values() {
1149 if let Entrypoint::AppPage {
1150 participating_page_files,
1151 ..
1152 } = entrypoint
1153 {
1154 matched_pages.extend(participating_page_files.iter().cloned());
1155 }
1156 }
1157
1158 let unmatched_pages = authored_pages
1159 .into_iter()
1160 .filter(|page| !matched_pages.contains(page))
1161 .collect::<Vec<_>>();
1162 if !unmatched_pages.is_empty() {
1163 UnmatchedAppPagesIssue {
1164 app_dir,
1165 pages: unmatched_pages,
1166 }
1167 .resolved_cell()
1168 .emit();
1169 }
1170 Ok(Vc::cell(retained_entrypoints))
1171}
1172
1173#[turbo_tasks::value]
1174struct MissingCanonicalInterceptionRoutesIssue {
1175 routes: Vec<(AppPath, AppPath, FileSystemPath)>,
1176}
1177
1178#[turbo_tasks::value]
1179struct IncompatibleParallelRouteSlotsIssue {
1180 app_dir: FileSystemPath,
1181 routes: Vec<(FileSystemPath, AppPath, Vec<RcStr>)>,
1182}
1183
1184#[async_trait]
1185#[turbo_tasks::value_impl]
1186impl Issue for IncompatibleParallelRouteSlotsIssue {
1187 async fn file_path(&self) -> Result<FileSystemPath> {
1188 Ok(self.routes[0].0.clone())
1189 }
1190
1191 fn stage(&self) -> IssueStage {
1192 IssueStage::AppStructure
1193 }
1194
1195 fn severity(&self) -> IssueSeverity {
1196 IssueSeverity::Error
1197 }
1198
1199 async fn title(&self) -> Result<StyledString> {
1200 Ok(StyledString::Text(rcstr!(
1201 "Parallel route slots cannot render the same URLs"
1202 )))
1203 }
1204
1205 async fn description(&self) -> Result<Option<StyledString>> {
1206 let mut routes_by_layout = FxIndexMap::<FileSystemPath, Vec<_>>::default();
1207 for (layout, route, missing_slots) in &self.routes {
1208 routes_by_layout
1209 .entry(layout.clone())
1210 .or_default()
1211 .push((route.clone(), missing_slots.clone()));
1212 }
1213
1214 let mut layouts = routes_by_layout
1215 .into_iter()
1216 .map(|(layout, mut routes)| {
1217 routes.sort_by_cached_key(|(route, _)| route.to_string());
1218 let layout_path = self
1219 .app_dir
1220 .get_path_to(&layout)
1221 .expect("parallel route layout should be within the app directory")
1222 .to_string();
1223 let routes = routes
1224 .into_iter()
1225 .map(|(route, missing_slots)| {
1226 let missing_slots = missing_slots
1227 .iter()
1228 .map(|slot| {
1229 if &**slot == "children" {
1230 slot.to_string()
1231 } else {
1232 format!("@{slot}")
1233 }
1234 })
1235 .collect::<Vec<_>>()
1236 .join(", ");
1237 format!(
1238 "- {route} is missing a matching page or default.tsx in \
1239 {missing_slots}"
1240 )
1241 })
1242 .collect::<Vec<_>>()
1243 .join("\n");
1244 (layout_path, routes)
1245 })
1246 .collect::<Vec<_>>();
1247 layouts.sort_by(|a, b| a.0.cmp(&b.0));
1248 let layouts = layouts
1249 .into_iter()
1250 .map(|(layout_path, routes)| format!("app/{layout_path}\n{routes}"))
1251 .collect::<Vec<_>>()
1252 .join("\n\n");
1253
1254 Ok(Some(StyledString::Text(
1255 format!(
1256 "The following layouts have parallel route slots that cannot render the same \
1257 URLs:\n{layouts}\n\nEvery URL matched by one slot must have a matching page or \
1258 default.tsx in every sibling slot."
1259 )
1260 .into(),
1261 )))
1262 }
1263}
1264
1265#[async_trait]
1266#[turbo_tasks::value_impl]
1267impl Issue for MissingCanonicalInterceptionRoutesIssue {
1268 async fn file_path(&self) -> Result<FileSystemPath> {
1269 Ok(self.routes[0].2.clone())
1270 }
1271
1272 fn stage(&self) -> IssueStage {
1273 IssueStage::AppStructure
1274 }
1275
1276 fn severity(&self) -> IssueSeverity {
1277 IssueSeverity::Error
1278 }
1279
1280 async fn title(&self) -> Result<StyledString> {
1281 Ok(StyledString::Text(rcstr!(
1282 "Interception routes must have a canonical route"
1283 )))
1284 }
1285
1286 async fn description(&self) -> Result<Option<StyledString>> {
1287 let routes = self
1288 .routes
1289 .iter()
1290 .map(|(interception_route, canonical_route, _)| {
1291 format!("- {interception_route} (expected {canonical_route})")
1292 })
1293 .collect::<Vec<_>>()
1294 .join("\n");
1295 Ok(Some(StyledString::Text(
1296 format!(
1297 "The following interception routes do not have a canonical \
1298 route:\n{routes}\n\nEvery interception route must have a matching \
1299 non-interception route so the URL can be loaded directly or refreshed."
1300 )
1301 .into(),
1302 )))
1303 }
1304}
1305
1306fn collect_authored_page_files(
1307 directory_tree: &PlainDirectoryTree,
1308 pages: &mut FxIndexSet<FileSystemPath>,
1309) {
1310 if let Some(page) = &directory_tree.modules.page {
1311 pages.insert(page.clone());
1312 }
1313
1314 for subdirectory in directory_tree.subdirectories.values() {
1315 collect_authored_page_files(subdirectory, pages);
1316 }
1317}
1318
1319#[turbo_tasks::value]
1320struct UnmatchedAppPagesIssue {
1321 app_dir: FileSystemPath,
1322 pages: Vec<FileSystemPath>,
1323}
1324
1325#[async_trait]
1326#[turbo_tasks::value_impl]
1327impl Issue for UnmatchedAppPagesIssue {
1328 async fn file_path(&self) -> Result<FileSystemPath> {
1329 Ok(self.pages[0].clone())
1330 }
1331
1332 fn stage(&self) -> IssueStage {
1333 IssueStage::AppStructure
1334 }
1335
1336 fn severity(&self) -> IssueSeverity {
1337 IssueSeverity::Error
1338 }
1339
1340 async fn title(&self) -> Result<StyledString> {
1341 Ok(StyledString::Text(rcstr!("Unmatched app pages")))
1342 }
1343
1344 async fn description(&self) -> Result<Option<StyledString>> {
1345 let page_paths = self
1346 .pages
1347 .iter()
1348 .map(|page| {
1349 let relative_path = self
1350 .app_dir
1351 .get_path_to(page)
1352 .expect("authored page should be within the app directory");
1353 format!("- app/{}", relative_path)
1354 })
1355 .collect::<Vec<_>>()
1356 .join("\n");
1357 Ok(Some(StyledString::Text(
1358 format!(
1359 "The following page files do not match any complete route:\n{page_paths}\n\nEvery \
1360 page must be part of at least one complete route. Add matching pages or default \
1361 files for the sibling parallel route slots, or remove the unreachable pages."
1362 )
1363 .into(),
1364 )))
1365 }
1366}
1367
1368#[turbo_tasks::value]
1369struct DuplicateParallelRouteIssue {
1370 app_dir: FileSystemPath,
1371 previously_inserted_page: AppPage,
1372 page: AppPage,
1373}
1374
1375#[async_trait]
1376#[turbo_tasks::value_impl]
1377impl Issue for DuplicateParallelRouteIssue {
1378 async fn file_path(&self) -> Result<FileSystemPath> {
1379 self.app_dir.join(&self.page.to_string())
1380 }
1381
1382 fn stage(&self) -> IssueStage {
1383 IssueStage::ProcessModule
1384 }
1385
1386 async fn title(&self) -> Result<StyledString> {
1387 Ok(StyledString::Text(
1388 format!(
1389 "You cannot have two parallel pages that resolve to the same path. Please check \
1390 {} and {}.",
1391 self.previously_inserted_page, self.page
1392 )
1393 .into(),
1394 ))
1395 }
1396}
1397
1398#[turbo_tasks::value]
1399struct MissingRootLayoutIssue {
1400 app_dir: FileSystemPath,
1401 page_path: FileSystemPath,
1402}
1403
1404#[async_trait]
1405#[turbo_tasks::value_impl]
1406impl Issue for MissingRootLayoutIssue {
1407 async fn file_path(&self) -> Result<FileSystemPath> {
1408 Ok(self.page_path.clone())
1409 }
1410
1411 fn stage(&self) -> IssueStage {
1412 IssueStage::AppStructure
1413 }
1414
1415 fn severity(&self) -> IssueSeverity {
1416 IssueSeverity::Error
1417 }
1418
1419 async fn title(&self) -> Result<StyledString> {
1420 let page_path = self
1421 .app_dir
1422 .get_path_to(&self.page_path)
1423 .context("page should be within the app directory")?;
1424
1425 Ok(StyledString::Text(
1426 format!(
1427 "{page_path} doesn't have a root layout. To fix this error, make sure every page \
1428 has a root layout."
1429 )
1430 .into(),
1431 ))
1432 }
1433}
1434
1435#[turbo_tasks::value]
1436struct MissingDefaultParallelRouteIssue {
1437 app_dir: FileSystemPath,
1438 app_page: AppPage,
1439 slot_name: RcStr,
1440}
1441
1442#[turbo_tasks::function]
1443fn missing_default_parallel_route_issue(
1444 app_dir: FileSystemPath,
1445 app_page: AppPage,
1446 slot_name: RcStr,
1447) -> Vc<MissingDefaultParallelRouteIssue> {
1448 MissingDefaultParallelRouteIssue {
1449 app_dir,
1450 app_page,
1451 slot_name,
1452 }
1453 .cell()
1454}
1455
1456#[async_trait]
1457#[turbo_tasks::value_impl]
1458impl Issue for MissingDefaultParallelRouteIssue {
1459 async fn file_path(&self) -> Result<FileSystemPath> {
1460 self.app_dir
1461 .join(&self.app_page.to_string())?
1462 .join(&format!("@{}", self.slot_name))
1463 }
1464
1465 fn stage(&self) -> IssueStage {
1466 IssueStage::AppStructure
1467 }
1468
1469 fn severity(&self) -> IssueSeverity {
1470 IssueSeverity::Error
1471 }
1472
1473 async fn title(&self) -> Result<StyledString> {
1474 Ok(StyledString::Text(
1475 format!(
1476 "Missing required default.js file for parallel route at {}/@{}",
1477 self.app_page, self.slot_name
1478 )
1479 .into(),
1480 ))
1481 }
1482
1483 async fn description(&self) -> Result<Option<StyledString>> {
1484 Ok(Some(StyledString::Stack(vec![
1485 StyledString::Text(
1486 format!(
1487 "The parallel route slot \"@{}\" is missing a default.js file. When using \
1488 parallel routes, each slot must have a default.js file to serve as a \
1489 fallback.",
1490 self.slot_name
1491 )
1492 .into(),
1493 ),
1494 StyledString::Text(
1495 format!(
1496 "Create a default.js file at: {}/@{}/default.js",
1497 self.app_page, self.slot_name
1498 )
1499 .into(),
1500 ),
1501 ])))
1502 }
1503
1504 fn documentation_link(&self) -> RcStr {
1505 rcstr!("https://nextjs.org/docs/messages/slot-missing-default")
1506 }
1507}
1508
1509fn page_path_except_parallel(loader_tree: &AppPageLoaderTree) -> Option<AppPage> {
1510 if loader_tree.page.iter().any(|v| {
1511 matches!(
1512 v,
1513 PageSegment::CatchAll(..)
1514 | PageSegment::OptionalCatchAll(..)
1515 | PageSegment::Parallel(..)
1516 )
1517 }) {
1518 return None;
1519 }
1520
1521 if loader_tree.modules.page.is_some() {
1522 return Some(loader_tree.page.clone());
1523 }
1524
1525 if let Some(children) = loader_tree.parallel_routes.get("children") {
1526 return page_path_except_parallel(children);
1527 }
1528
1529 None
1530}
1531
1532fn has_child_routes(directory_tree: &PlainDirectoryTree) -> bool {
1536 for (name, subdirectory) in &directory_tree.subdirectories {
1537 if is_parallel_route(name) {
1539 continue;
1540 }
1541
1542 if is_group_route(name) {
1544 if has_child_routes(subdirectory) {
1546 return true;
1547 }
1548 continue;
1549 }
1550
1551 return true;
1553 }
1554
1555 false
1556}
1557
1558fn has_declared_children_slot(directory_tree: &PlainDirectoryTree) -> bool {
1562 directory_tree.modules.page.is_some()
1563 || directory_tree.modules.default.is_some()
1564 || directory_tree
1565 .subdirectories
1566 .iter()
1567 .filter(|(name, _)| !is_parallel_route(name))
1568 .any(|(_, subdirectory)| subdirectory.contains_page_or_default)
1569}
1570
1571fn collect_named_slots_at_level(
1574 directory_tree: &PlainDirectoryTree,
1575 slots: &mut FxIndexSet<RcStr>,
1576) {
1577 for (name, subdirectory) in &directory_tree.subdirectories {
1578 if let Some(slot) = match_parallel_route(name) {
1579 if subdirectory.contains_page_or_default {
1580 slots.insert(slot.into());
1581 }
1582 } else if is_group_route(name) {
1583 collect_named_slots_at_level(subdirectory, slots);
1584 }
1585 }
1586}
1587
1588fn collect_declared_parallel_route_slots(
1592 directory_tree: &PlainDirectoryTree,
1593 slots_by_layout: &mut FxIndexMap<FileSystemPath, FxIndexSet<RcStr>>,
1594) {
1595 if let Some(layout) = &directory_tree.modules.layout {
1596 let mut slots = FxIndexSet::default();
1597 if has_declared_children_slot(directory_tree) {
1598 slots.insert(rcstr!("children"));
1599 }
1600 collect_named_slots_at_level(directory_tree, &mut slots);
1601 slots_by_layout.insert(layout.clone(), slots);
1602 }
1603
1604 for subdirectory in directory_tree.subdirectories.values() {
1605 collect_declared_parallel_route_slots(subdirectory, slots_by_layout);
1606 }
1607}
1608
1609async fn check_duplicate(
1610 duplicate: &mut FxHashMap<AppPath, AppPage>,
1611 loader_tree: &AppPageLoaderTree,
1612 app_dir: FileSystemPath,
1613) -> Result<()> {
1614 let page_path = page_path_except_parallel(loader_tree);
1615
1616 if let Some(page_path) = page_path
1617 && let Some(prev) = duplicate.insert(AppPath::from(page_path.clone()), page_path.clone())
1618 && prev != page_path
1619 {
1620 DuplicateParallelRouteIssue {
1621 app_dir: app_dir.clone(),
1622 previously_inserted_page: prev.clone(),
1623 page: loader_tree.page.clone(),
1624 }
1625 .resolved_cell()
1626 .emit();
1627 }
1628
1629 Ok(())
1630}
1631
1632#[turbo_tasks::value(transparent)]
1633struct AppPageLoaderTreeOption(Option<ResolvedVc<AppPageLoaderTree>>);
1634
1635#[turbo_tasks::function]
1637async fn directory_tree_to_loader_tree(
1638 app_dir: FileSystemPath,
1639 global_metadata: Vc<GlobalMetadata>,
1640 directory_name: RcStr,
1641 directory_tree: Vc<DirectoryTree>,
1642 app_page: AppPage,
1643 for_app_path: AppPath,
1645 explicit_parallel_route_children: Vc<bool>,
1646 strict_route_matching: Vc<bool>,
1647) -> Result<Vc<AppPageLoaderTreeOption>> {
1648 let plain_tree_vc = directory_tree.into_plain();
1649 let plain_tree = &*plain_tree_vc.await?;
1650 let strict_route_matching = *strict_route_matching.await?;
1651 let mut missing_defaults = Vec::new();
1652 let tree = directory_tree_to_loader_tree_internal(
1653 app_dir.clone(),
1654 global_metadata,
1655 directory_name,
1656 plain_tree,
1657 app_page.clone(),
1658 for_app_path,
1659 *explicit_parallel_route_children.await?,
1660 AppDirModules::default(),
1661 Some(&plain_tree.url_tree),
1662 &mut missing_defaults,
1663 )
1664 .await?;
1665
1666 if !strict_route_matching {
1669 for (page, slot) in missing_defaults {
1670 missing_default_parallel_route_issue(app_dir.clone(), page, slot)
1671 .to_resolved()
1672 .await?
1673 .emit();
1674 }
1675 }
1676
1677 Ok(Vc::cell(tree.map(AppPageLoaderTree::resolved_cell)))
1678}
1679
1680async fn check_and_update_module_references(
1695 app_dir: FileSystemPath,
1696 module: &mut Option<FileSystemPath>,
1697 parent_module: &mut Option<FileSystemPath>,
1698 file_path: &str,
1699 is_first_layer_group_route: bool,
1700) -> Result<()> {
1701 match (module.as_mut(), parent_module.as_mut()) {
1702 (Some(module), _) => *parent_module = Some(module.clone()),
1704 (None, Some(parent_module)) if is_first_layer_group_route => {
1707 *module = Some(parent_module.clone())
1708 }
1709 (None, Some(_)) => {}
1712 (None, None) => {
1716 let default_page = get_next_package(app_dir).await?.join(file_path)?;
1717 *module = Some(default_page.clone());
1718 *parent_module = Some(default_page);
1719 }
1720 }
1721
1722 Ok(())
1723}
1724
1725async fn check_and_update_global_module_references(
1733 app_dir: FileSystemPath,
1734 module: &mut Option<FileSystemPath>,
1735 file_path: &str,
1736) -> Result<()> {
1737 if module.is_none() {
1738 *module = Some(get_next_package(app_dir).await?.join(file_path)?);
1739 }
1740
1741 Ok(())
1742}
1743
1744async fn directory_tree_to_loader_tree_internal(
1745 app_dir: FileSystemPath,
1746 global_metadata: Vc<GlobalMetadata>,
1747 directory_name: RcStr,
1748 directory_tree: &PlainDirectoryTree,
1749 app_page: AppPage,
1750 for_app_path: AppPath,
1752 explicit_parallel_route_children: bool,
1753 mut parent_modules: AppDirModules,
1754 url_tree: Option<&UrlSegmentTree>,
1755 missing_defaults: &mut Vec<(AppPage, RcStr)>,
1756) -> Result<Option<AppPageLoaderTree>> {
1757 let app_path = AppPath::from(app_page.clone());
1758
1759 if !for_app_path.contains(&app_path) {
1760 return Ok(None);
1761 }
1762
1763 let mut modules = directory_tree.modules.clone();
1764
1765 modules.metadata.base_page = Some(app_page.clone());
1768
1769 let is_root_directory = app_page.is_root();
1771
1772 let is_first_layer_group_route = app_page.is_first_layer_group_route();
1774
1775 if is_root_directory || is_first_layer_group_route {
1778 check_and_update_module_references(
1779 app_dir.clone(),
1780 &mut modules.not_found,
1781 &mut parent_modules.not_found,
1782 "dist/client/components/builtin/not-found.js",
1783 is_first_layer_group_route,
1784 )
1785 .await?;
1786
1787 check_and_update_module_references(
1788 app_dir.clone(),
1789 &mut modules.forbidden,
1790 &mut parent_modules.forbidden,
1791 "dist/client/components/builtin/forbidden.js",
1792 is_first_layer_group_route,
1793 )
1794 .await?;
1795
1796 check_and_update_module_references(
1797 app_dir.clone(),
1798 &mut modules.unauthorized,
1799 &mut parent_modules.unauthorized,
1800 "dist/client/components/builtin/unauthorized.js",
1801 is_first_layer_group_route,
1802 )
1803 .await?;
1804 }
1805
1806 if is_root_directory {
1807 check_and_update_global_module_references(
1808 app_dir.clone(),
1809 &mut modules.global_error,
1810 "dist/client/components/builtin/global-error.js",
1811 )
1812 .await?;
1813 }
1814
1815 let static_siblings: Vec<RcStr> = if is_dynamic_segment(&directory_name) {
1819 url_tree
1820 .map(|t| {
1821 t.static_children()
1822 .into_iter()
1823 .filter(|s| s != &directory_name)
1824 .collect()
1825 })
1826 .unwrap_or_default()
1827 } else {
1828 Vec::new()
1830 };
1831
1832 let mut tree = AppPageLoaderTree {
1833 page: app_page.clone(),
1834 segment: directory_name.clone(),
1835 parallel_routes: FxIndexMap::default(),
1836 modules: modules.without_leaves(),
1837 global_metadata: global_metadata.to_resolved().await?,
1838 static_siblings,
1839 };
1840
1841 let current_level_is_parallel_route = is_parallel_route(&directory_name);
1842
1843 if current_level_is_parallel_route {
1844 tree.segment = rcstr!("(__SLOT__)");
1845 }
1846
1847 if let Some(page) = (app_path == for_app_path || app_path.is_catchall())
1848 .then_some(modules.page)
1849 .flatten()
1850 {
1851 tree.parallel_routes.insert(
1852 rcstr!("children"),
1853 AppPageLoaderTree {
1854 page: app_page.clone(),
1855 segment: rcstr!("__PAGE__"),
1856 parallel_routes: FxIndexMap::default(),
1857 modules: AppDirModules {
1858 page: Some(page),
1859 metadata: modules.metadata,
1860 ..Default::default()
1861 },
1862 global_metadata: global_metadata.to_resolved().await?,
1863 static_siblings: Vec::new(),
1864 },
1865 );
1866 }
1867
1868 let mut duplicate = FxHashMap::default();
1869
1870 for (subdir_name, subdirectory) in &directory_tree.subdirectories {
1871 let parallel_route_key = match_parallel_route(subdir_name);
1872
1873 let mut child_app_page = app_page.clone();
1874 let mut illegal_path_error = None;
1875
1876 if let Err(e) = child_app_page.push_str(&normalize_underscore(subdir_name)) {
1880 illegal_path_error = Some(e);
1881 }
1882
1883 let child_url_tree: Option<&UrlSegmentTree> =
1885 if directory_name.is_empty() || is_url_transparent_segment(&directory_name) {
1886 url_tree
1887 } else {
1888 url_tree.and_then(|t| t.get_child(&directory_name))
1889 };
1890
1891 let subtree = Box::pin(directory_tree_to_loader_tree_internal(
1892 app_dir.clone(),
1893 global_metadata,
1894 subdir_name.clone(),
1895 subdirectory,
1896 child_app_page.clone(),
1897 for_app_path.clone(),
1898 explicit_parallel_route_children,
1899 parent_modules.clone(),
1900 child_url_tree,
1901 missing_defaults,
1902 ))
1903 .await?;
1904
1905 if let Some(illegal_path) = subtree.as_ref().and(illegal_path_error) {
1906 return Err(illegal_path);
1907 }
1908
1909 if let Some(subtree) = subtree {
1910 if let Some(key) = parallel_route_key {
1911 let is_inside_catchall = app_page.is_catchall();
1922
1923 let is_leaf_segment = !has_child_routes(directory_tree);
1931
1932 let slot_has_children = has_child_routes(subdirectory);
1943
1944 if key != "children"
1945 && subdirectory.modules.default.is_none()
1946 && !is_inside_catchall
1947 && !is_leaf_segment
1948 && !slot_has_children
1949 {
1950 missing_defaults.push((app_page.clone(), key.into()));
1951 }
1952
1953 tree.parallel_routes.insert(key.into(), subtree);
1954 continue;
1955 }
1956
1957 if is_group_route(subdir_name) && !subtree.has_page() {
1959 continue;
1960 }
1961
1962 if subtree.has_page() {
1963 check_duplicate(&mut duplicate, &subtree, app_dir.clone()).await?;
1964 }
1965
1966 if let Some(current_tree) = tree.parallel_routes.get("children") {
1967 if current_tree.has_only_catchall()
1968 && (!subtree.has_only_catchall()
1969 || current_tree.get_specificity() < subtree.get_specificity())
1970 {
1971 tree.parallel_routes
1972 .insert(rcstr!("children"), subtree.clone());
1973 }
1974 } else {
1975 tree.parallel_routes.insert(rcstr!("children"), subtree);
1976 }
1977 } else if let Some(key) = parallel_route_key {
1978 bail!(
1979 "missing page or default for parallel route `{}` (page: {})",
1980 key,
1981 app_page
1982 );
1983 }
1984 }
1985
1986 let is_interception_host = !app_path.contains_interception()
1990 && tree
1991 .parallel_routes
1992 .iter()
1993 .any(|(_, parallel_tree)| parallel_tree.is_intercepting());
1994
1995 if is_interception_host {
1996 let keys_to_replace = tree
1997 .parallel_routes
1998 .iter()
1999 .filter(|(_, parallel_tree)| !parallel_tree.is_intercepting())
2000 .map(|(key, _)| key.clone())
2001 .collect::<Vec<_>>();
2002 let is_inside_catchall = app_page.is_catchall();
2003 let is_leaf_segment = !has_child_routes(directory_tree);
2004
2005 for key in keys_to_replace {
2006 let subdir_name: RcStr = format!("@{key}").into();
2007
2008 let default = if key == "children" {
2009 modules.default.clone()
2010 } else if let Some(subdirectory) = directory_tree.subdirectories.get(&subdir_name) {
2011 subdirectory.modules.default.clone()
2012 } else {
2013 None
2014 };
2015 let slot_has_children = directory_tree
2016 .subdirectories
2017 .get(&subdir_name)
2018 .is_some_and(has_child_routes);
2019
2020 if default.is_none()
2025 && key != "children"
2026 && !is_inside_catchall
2027 && !is_leaf_segment
2028 && !slot_has_children
2029 {
2030 missing_defaults.push((app_page.clone(), key.clone()));
2031 }
2032
2033 tree.parallel_routes.insert(
2034 key.clone(),
2035 retained_route_tree(app_dir.clone(), global_metadata, app_page.clone()).await?,
2036 );
2037 }
2038 }
2039
2040 if tree.parallel_routes.is_empty() {
2041 if modules.default.is_some() || current_level_is_parallel_route {
2042 tree = default_route_tree(
2043 app_dir.clone(),
2044 global_metadata,
2045 app_page.clone(),
2046 modules.default.clone(),
2047 rcstr!("children"),
2048 for_app_path.clone(),
2049 )
2050 .await?;
2051 } else {
2052 return Ok(None);
2053 }
2054 } else if tree.parallel_routes.get("children").is_none()
2055 && (!explicit_parallel_route_children || has_declared_children_slot(directory_tree))
2056 {
2057 let children = if is_interception_host {
2061 retained_route_tree(app_dir.clone(), global_metadata, app_page.clone()).await?
2062 } else {
2063 default_route_tree(
2064 app_dir.clone(),
2065 global_metadata,
2066 app_page.clone(),
2067 modules.default.clone(),
2068 rcstr!("children"),
2069 for_app_path.clone(),
2070 )
2071 .await?
2072 };
2073 tree.parallel_routes.insert(rcstr!("children"), children);
2074 }
2075
2076 Ok(Some(tree))
2077}
2078
2079async fn default_route_tree(
2080 app_dir: FileSystemPath,
2081 global_metadata: Vc<GlobalMetadata>,
2082 app_page: AppPage,
2083 default_component: Option<FileSystemPath>,
2084 slot_name: RcStr,
2085 for_app_path: AppPath,
2086) -> Result<AppPageLoaderTree> {
2087 let default = if let Some(default) = default_component {
2088 default
2089 } else {
2090 let contains_interception = for_app_path.contains_interception();
2091
2092 let default_file = if contains_interception && slot_name == "children" {
2097 "dist/client/components/builtin/default-null.js"
2098 } else {
2099 "dist/client/components/builtin/default.js"
2100 };
2101
2102 get_next_package(app_dir).await?.join(default_file)?
2103 };
2104
2105 synthetic_default_route_tree(global_metadata, app_page, default).await
2106}
2107
2108async fn retained_route_tree(
2109 app_dir: FileSystemPath,
2110 global_metadata: Vc<GlobalMetadata>,
2111 app_page: AppPage,
2112) -> Result<AppPageLoaderTree> {
2113 let default_null = get_next_package(app_dir)
2114 .await?
2115 .join("dist/client/components/builtin/default-null.js")?;
2116 synthetic_default_route_tree(global_metadata, app_page, default_null).await
2117}
2118
2119async fn synthetic_default_route_tree(
2120 global_metadata: Vc<GlobalMetadata>,
2121 app_page: AppPage,
2122 default: FileSystemPath,
2123) -> Result<AppPageLoaderTree> {
2124 Ok(AppPageLoaderTree {
2125 page: app_page,
2126 segment: rcstr!("__DEFAULT__"),
2127 parallel_routes: FxIndexMap::default(),
2128 modules: AppDirModules {
2129 default: Some(default),
2130 ..Default::default()
2131 },
2132 global_metadata: global_metadata.to_resolved().await?,
2133 static_siblings: Vec::new(),
2134 })
2135}
2136
2137#[turbo_tasks::function]
2138async fn directory_tree_to_entrypoints_internal(
2139 app_dir: FileSystemPath,
2140 global_metadata: ResolvedVc<GlobalMetadata>,
2141 is_global_not_found_enabled: Vc<bool>,
2142 explicit_parallel_route_children: Vc<bool>,
2143 strict_route_matching: Vc<bool>,
2144 next_mode: Vc<NextMode>,
2145 directory_name: RcStr,
2146 directory_tree: Vc<DirectoryTree>,
2147 app_page: AppPage,
2148 root_layouts: ResolvedVc<FileSystemPathVec>,
2149 root_params: ResolvedVc<RootParamVecOption>,
2150) -> Result<Vc<Entrypoints>> {
2151 let span = tracing::info_span!("build layout trees", name = display(&app_page));
2152 directory_tree_to_entrypoints_internal_untraced(
2153 app_dir,
2154 global_metadata,
2155 is_global_not_found_enabled,
2156 explicit_parallel_route_children,
2157 strict_route_matching,
2158 next_mode,
2159 directory_name,
2160 directory_tree,
2161 app_page,
2162 root_layouts,
2163 root_params,
2164 )
2165 .instrument(span)
2166 .await
2167}
2168
2169async fn directory_tree_to_entrypoints_internal_untraced(
2170 app_dir: FileSystemPath,
2171 global_metadata: ResolvedVc<GlobalMetadata>,
2172 is_global_not_found_enabled: Vc<bool>,
2173 explicit_parallel_route_children: Vc<bool>,
2174 strict_route_matching: Vc<bool>,
2175 next_mode: Vc<NextMode>,
2176 directory_name: RcStr,
2177 directory_tree: Vc<DirectoryTree>,
2178 app_page: AppPage,
2179 root_layouts: ResolvedVc<FileSystemPathVec>,
2180 root_params: ResolvedVc<RootParamVecOption>,
2181) -> Result<Vc<Entrypoints>> {
2182 let mut result = FxIndexMap::default();
2183
2184 let directory_tree_vc = directory_tree;
2185 let directory_tree = &*directory_tree.await?;
2186
2187 let subdirectories = &directory_tree.subdirectories;
2188 let modules = &directory_tree.modules;
2189 let root_layouts = if let Some(layout) = &modules.layout {
2193 let mut layouts = root_layouts.owned().await?;
2194 layouts.push(layout.clone());
2195 ResolvedVc::cell(layouts)
2196 } else {
2197 root_layouts
2198 };
2199
2200 let root_params = if root_params.await?.is_none() && (*root_layouts.await?).len() == 1 {
2202 ResolvedVc::cell(Some(
2205 app_page
2206 .0
2207 .iter()
2208 .filter_map(|segment| match segment {
2209 PageSegment::Dynamic(param)
2210 | PageSegment::CatchAll(param)
2211 | PageSegment::OptionalCatchAll(param) => Some(param.clone()),
2212 _ => None,
2213 })
2214 .collect::<Vec<RcStr>>(),
2215 ))
2216 } else {
2217 root_params
2218 };
2219
2220 if let Some(page_path) = &modules.page {
2221 if root_layouts.await?.is_empty() {
2222 MissingRootLayoutIssue {
2223 app_dir: app_dir.clone(),
2224 page_path: page_path.clone(),
2225 }
2226 .resolved_cell()
2227 .emit();
2228 }
2229
2230 let app_path = AppPath::from(app_page.clone());
2231
2232 let loader_tree = *directory_tree_to_loader_tree(
2233 app_dir.clone(),
2234 *global_metadata,
2235 directory_name.clone(),
2236 directory_tree_vc,
2237 app_page.clone(),
2238 app_path,
2239 explicit_parallel_route_children,
2240 strict_route_matching,
2241 )
2242 .await?;
2243
2244 let loader_tree =
2245 loader_tree.context("loader tree should be created for a page/default")?;
2246 let mut participating_page_files = FxIndexSet::default();
2247 loader_tree
2248 .await?
2249 .collect_page_files(&mut participating_page_files);
2250 add_app_page(
2251 app_dir.clone(),
2252 &mut result,
2253 app_page.complete(PageType::Page)?,
2254 loader_tree,
2255 participating_page_files.into_iter().collect(),
2256 root_params,
2257 );
2258 }
2259
2260 if let Some(route) = &modules.route {
2261 add_app_route(
2262 app_dir.clone(),
2263 &mut result,
2264 app_page.complete(PageType::Route)?,
2265 route.clone(),
2266 root_params,
2267 );
2268 }
2269
2270 let Metadata {
2271 icon,
2272 apple,
2273 twitter,
2274 open_graph,
2275 sitemap,
2276 base_page: _,
2277 } = &modules.metadata;
2278
2279 for meta in sitemap
2280 .iter()
2281 .cloned()
2282 .chain(icon.iter().cloned().map(MetadataItem::from))
2283 .chain(apple.iter().cloned().map(MetadataItem::from))
2284 .chain(twitter.iter().cloned().map(MetadataItem::from))
2285 .chain(open_graph.iter().cloned().map(MetadataItem::from))
2286 {
2287 let app_page = app_page.clone_push_str(&get_metadata_route_name(meta.clone()).await?)?;
2288
2289 add_app_metadata_route(
2290 app_dir.clone(),
2291 &mut result,
2292 normalize_metadata_route(app_page)?,
2293 meta,
2294 root_params,
2295 );
2296 }
2297
2298 if app_page.is_root() {
2300 let GlobalMetadata {
2301 favicon,
2302 robots,
2303 manifest,
2304 } = &*global_metadata.await?;
2305
2306 for meta in favicon.iter().chain(robots.iter()).chain(manifest.iter()) {
2307 let app_page =
2308 app_page.clone_push_str(&get_metadata_route_name(meta.clone()).await?)?;
2309
2310 add_app_metadata_route(
2311 app_dir.clone(),
2312 &mut result,
2313 normalize_metadata_route(app_page)?,
2314 meta.clone(),
2315 root_params,
2316 );
2317 }
2318
2319 let mut modules = directory_tree.modules.clone();
2320
2321 if modules.layout.is_none() {
2323 modules.layout = Some(
2324 get_next_package(app_dir.clone())
2325 .await?
2326 .join("dist/client/components/builtin/layout.js")?,
2327 );
2328 }
2329
2330 if modules.not_found.is_none() {
2331 modules.not_found = Some(
2332 get_next_package(app_dir.clone())
2333 .await?
2334 .join("dist/client/components/builtin/not-found.js")?,
2335 );
2336 }
2337 if modules.forbidden.is_none() {
2338 modules.forbidden = Some(
2339 get_next_package(app_dir.clone())
2340 .await?
2341 .join("dist/client/components/builtin/forbidden.js")?,
2342 );
2343 }
2344 if modules.unauthorized.is_none() {
2345 modules.unauthorized = Some(
2346 get_next_package(app_dir.clone())
2347 .await?
2348 .join("dist/client/components/builtin/unauthorized.js")?,
2349 );
2350 }
2351 if modules.global_error.is_none() {
2352 modules.global_error = Some(
2353 get_next_package(app_dir.clone())
2354 .await?
2355 .join("dist/client/components/builtin/global-error.js")?,
2356 );
2357 }
2358
2359 let is_global_not_found_enabled = *is_global_not_found_enabled.await?;
2364 let use_global_not_found =
2365 is_global_not_found_enabled || modules.global_not_found.is_some();
2366
2367 let not_found_root_modules = modules.without_leaves();
2368 let not_found_tree = AppPageLoaderTree {
2369 page: app_page.clone(),
2370 segment: directory_name.clone(),
2371 parallel_routes: fxindexmap! {
2372 rcstr!("children") => AppPageLoaderTree {
2373 page: app_page.clone(),
2374 segment: rcstr!("/_not-found"),
2375 parallel_routes: fxindexmap! {
2376 rcstr!("children") => AppPageLoaderTree {
2377 page: app_page.clone(),
2380 segment: rcstr!("__PAGE__"),
2381 parallel_routes: FxIndexMap::default(),
2382 modules: if use_global_not_found {
2383 AppDirModules {
2386 page: Some(get_next_package(app_dir.clone())
2388 .await?
2389 .join("dist/client/components/builtin/empty-stub.js")?,
2390 ),
2391 ..Default::default()
2392 }
2393 } else {
2394 AppDirModules {
2397 page: match modules.not_found {
2398 Some(v) => Some(v),
2399 None => Some(get_next_package(app_dir.clone())
2400 .await?
2401 .join("dist/client/components/builtin/not-found.js")?,
2402 ),
2403 },
2404 ..Default::default()
2405 }
2406 },
2407 global_metadata,
2408 static_siblings: Vec::new(),
2409 }
2410 },
2411 modules: AppDirModules {
2412 ..Default::default()
2413 },
2414 global_metadata,
2415 static_siblings: Vec::new(),
2416 },
2417 },
2418 modules: AppDirModules {
2419 layout: if use_global_not_found {
2424 match modules.global_not_found {
2425 Some(v) => Some(v),
2426 None => Some(
2427 get_next_package(app_dir.clone())
2428 .await?
2429 .join("dist/client/components/builtin/global-not-found.js")?,
2430 ),
2431 }
2432 } else {
2433 modules.layout
2434 },
2435 ..not_found_root_modules
2436 },
2437 global_metadata,
2438 static_siblings: Vec::new(),
2439 }
2440 .resolved_cell();
2441
2442 {
2443 let app_page = app_page
2444 .clone_push_str("_not-found")?
2445 .complete(PageType::Page)?;
2446
2447 add_app_page(
2448 app_dir.clone(),
2449 &mut result,
2450 app_page,
2451 not_found_tree,
2452 Vec::new(),
2453 root_params,
2454 );
2455 }
2456
2457 if matches!(*next_mode.await?, NextMode::Build) {
2462 let next_package = get_next_package(app_dir.clone()).await?;
2465 let global_error_tree = AppPageLoaderTree {
2466 page: app_page.clone(),
2467 segment: directory_name.clone(),
2468 parallel_routes: fxindexmap! {
2469 rcstr!("children") => AppPageLoaderTree {
2470 page: app_page.clone(),
2471 segment: rcstr!("__PAGE__"),
2472 parallel_routes: FxIndexMap::default(),
2473 modules: AppDirModules {
2474 page: Some(next_package
2475 .join("dist/client/components/builtin/app-error.js")?),
2476 ..Default::default()
2477 },
2478 global_metadata,
2479 static_siblings: Vec::new(),
2480 }
2481 },
2482 modules: AppDirModules {
2485 global_error: modules.global_error.clone(),
2486 ..Default::default()
2487 },
2488 global_metadata,
2489 static_siblings: Vec::new(),
2490 }
2491 .resolved_cell();
2492
2493 let app_global_error_page = app_page
2494 .clone_push_str("_global-error")?
2495 .complete(PageType::Page)?;
2496 add_app_page(
2497 app_dir.clone(),
2498 &mut result,
2499 app_global_error_page,
2500 global_error_tree,
2501 Vec::new(),
2502 root_params,
2503 );
2504 }
2505 }
2506
2507 let app_page = &app_page;
2508 let directory_name = &directory_name;
2509 let subdirectories = subdirectories
2510 .iter()
2511 .map(|(subdir_name, &subdirectory)| {
2512 let app_dir = app_dir.clone();
2513
2514 async move {
2515 let mut child_app_page = app_page.clone();
2516 let mut illegal_path = None;
2517
2518 if let Err(e) = child_app_page.push_str(&normalize_underscore(subdir_name)) {
2522 illegal_path = Some(e);
2523 }
2524
2525 let map = directory_tree_to_entrypoints_internal(
2526 app_dir.clone(),
2527 *global_metadata,
2528 is_global_not_found_enabled,
2529 explicit_parallel_route_children,
2530 strict_route_matching,
2531 next_mode,
2532 subdir_name.clone(),
2533 *subdirectory,
2534 child_app_page.clone(),
2535 *root_layouts,
2536 *root_params,
2537 )
2538 .await?;
2539
2540 if let Some(illegal_path) = illegal_path
2541 && !map.is_empty()
2542 {
2543 return Err(illegal_path);
2544 }
2545
2546 let mut loader_trees = Vec::new();
2547
2548 for (_, entrypoint) in map.iter() {
2549 if let Entrypoint::AppPage { ref pages, .. } = *entrypoint {
2550 for page in pages {
2551 let app_path = AppPath::from(page.clone());
2552
2553 let loader_tree = directory_tree_to_loader_tree(
2554 app_dir.clone(),
2555 *global_metadata,
2556 directory_name.clone(),
2557 directory_tree_vc,
2558 app_page.clone(),
2559 app_path,
2560 explicit_parallel_route_children,
2561 strict_route_matching,
2562 );
2563 loader_trees.push(loader_tree);
2564 }
2565 }
2566 }
2567 Ok((map, loader_trees))
2568 }
2569 })
2570 .try_join()
2571 .await?;
2572
2573 for (map, loader_trees) in subdirectories.iter() {
2574 let mut i = 0;
2575 for (_, entrypoint) in map.iter() {
2576 match entrypoint {
2577 Entrypoint::AppPage {
2578 pages,
2579 loader_tree: _,
2580 participating_page_files: child_participating_page_files,
2581 root_params,
2582 } => {
2583 for page in pages {
2584 let loader_tree = *loader_trees[i].await?;
2585 i += 1;
2586
2587 let loader_tree = loader_tree
2588 .context("loader tree should be created for a page/default")?;
2589 let mut participating_page_files = FxIndexSet::default();
2590 loader_tree
2591 .await?
2592 .collect_page_files(&mut participating_page_files);
2593 participating_page_files
2594 .extend(child_participating_page_files.iter().cloned());
2595 add_app_page(
2596 app_dir.clone(),
2597 &mut result,
2598 page.clone(),
2599 loader_tree,
2600 participating_page_files.into_iter().collect(),
2601 *root_params,
2602 );
2603 }
2604 }
2605 Entrypoint::AppRoute {
2606 page,
2607 path,
2608 root_params,
2609 } => {
2610 add_app_route(
2611 app_dir.clone(),
2612 &mut result,
2613 page.clone(),
2614 path.clone(),
2615 *root_params,
2616 );
2617 }
2618 Entrypoint::AppMetadata {
2619 page,
2620 metadata,
2621 root_params,
2622 } => {
2623 add_app_metadata_route(
2624 app_dir.clone(),
2625 &mut result,
2626 page.clone(),
2627 metadata.clone(),
2628 *root_params,
2629 );
2630 }
2631 }
2632 }
2633 }
2634 Ok(Vc::cell(result))
2635}
2636
2637#[turbo_tasks::function]
2639pub async fn get_global_metadata(
2640 app_dir: FileSystemPath,
2641 page_extensions: Vc<Vec<RcStr>>,
2642) -> Result<Vc<GlobalMetadata>> {
2643 let DirectoryContent::Entries(entries) = &*app_dir.read_dir().await? else {
2644 bail!("app_dir must be a directory")
2645 };
2646 let mut metadata = GlobalMetadata::default();
2647
2648 for (basename, entry) in entries {
2649 let DirectoryEntry::File(file) = entry else {
2650 continue;
2651 };
2652
2653 let Some(GlobalMetadataFileMatch {
2654 metadata_type,
2655 dynamic,
2656 }) = match_global_metadata_file(basename, &page_extensions.await?)
2657 else {
2658 continue;
2659 };
2660
2661 let entry = match metadata_type {
2662 "favicon" => &mut metadata.favicon,
2663 "manifest" => &mut metadata.manifest,
2664 "robots" => &mut metadata.robots,
2665 _ => continue,
2666 };
2667
2668 if dynamic {
2669 *entry = Some(MetadataItem::Dynamic { path: file.clone() });
2670 } else {
2671 *entry = Some(MetadataItem::Static { path: file.clone() });
2672 }
2673 }
2675
2676 Ok(metadata.cell())
2677}
2678
2679#[turbo_tasks::value(shared)]
2680struct DirectoryTreeIssue {
2681 pub severity: IssueSeverity,
2682 pub app_dir: FileSystemPath,
2683 pub message: ResolvedVc<StyledString>,
2684}
2685
2686#[async_trait]
2687#[turbo_tasks::value_impl]
2688impl Issue for DirectoryTreeIssue {
2689 fn severity(&self) -> IssueSeverity {
2690 self.severity
2691 }
2692
2693 async fn title(&self) -> Result<StyledString> {
2694 Ok(StyledString::Text(rcstr!(
2695 "An issue occurred while preparing your Next.js app"
2696 )))
2697 }
2698
2699 fn stage(&self) -> IssueStage {
2700 IssueStage::AppStructure
2701 }
2702
2703 async fn file_path(&self) -> Result<FileSystemPath> {
2704 Ok(self.app_dir.clone())
2705 }
2706
2707 async fn description(&self) -> Result<Option<StyledString>> {
2708 Ok(Some((*self.message.await?).clone()))
2709 }
2710}