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_layouts: ResolvedVc<FileSystemPathVec>,
697 root_params: ResolvedVc<RootParamVecOption>,
698 },
699 AppMetadata {
700 page: AppPage,
701 metadata: MetadataItem,
702 root_params: ResolvedVc<RootParamVecOption>,
703 },
704}
705
706impl Entrypoint {
707 pub fn page(&self) -> &AppPage {
708 match self {
709 Entrypoint::AppPage { pages, .. } => pages.first().unwrap(),
710 Entrypoint::AppRoute { page, .. } => page,
711 Entrypoint::AppMetadata { page, .. } => page,
712 }
713 }
714 pub fn root_params(&self) -> ResolvedVc<RootParamVecOption> {
715 match self {
716 Entrypoint::AppPage { root_params, .. } => *root_params,
717 Entrypoint::AppRoute { root_params, .. } => *root_params,
718 Entrypoint::AppMetadata { root_params, .. } => *root_params,
719 }
720 }
721}
722
723#[turbo_tasks::value(transparent)]
724pub struct Entrypoints(
725 #[bincode(with = "turbo_bincode::indexmap")] FxIndexMap<AppPath, Entrypoint>,
726);
727
728fn is_parallel_route(name: &str) -> bool {
729 name.starts_with('@')
730}
731
732fn is_group_route(name: &str) -> bool {
733 name.starts_with('(') && name.ends_with(')')
734}
735
736fn is_url_transparent_segment(name: &str) -> bool {
740 is_group_route(name) || is_parallel_route(name)
741}
742
743fn is_dynamic_segment(name: &str) -> bool {
744 name.starts_with('[') && name.ends_with(']')
745}
746
747fn match_parallel_route(name: &str) -> Option<&str> {
748 name.strip_prefix('@')
749}
750
751fn conflict_issue(
752 app_dir: FileSystemPath,
753 e: &'_ OccupiedEntry<'_, AppPath, Entrypoint>,
754 a: &str,
755 b: &str,
756 value_a: &AppPage,
757 value_b: &AppPage,
758) {
759 let item_names = if a == b {
760 format!("{a}s")
761 } else {
762 format!("{a} and {b}")
763 };
764
765 DirectoryTreeIssue {
766 app_dir,
767 message: StyledString::Text(
768 format!(
769 "Conflicting {} at {}: {a} at {value_a} and {b} at {value_b}",
770 item_names,
771 e.key(),
772 )
773 .into(),
774 )
775 .resolved_cell(),
776 severity: IssueSeverity::Error,
777 }
778 .resolved_cell()
779 .emit();
780}
781
782fn add_app_page(
783 app_dir: FileSystemPath,
784 result: &mut FxIndexMap<AppPath, Entrypoint>,
785 page: AppPage,
786 loader_tree: ResolvedVc<AppPageLoaderTree>,
787 participating_page_files: Vec<FileSystemPath>,
788 root_params: ResolvedVc<RootParamVecOption>,
789) {
790 let mut e = match result.entry(page.clone().into()) {
791 Entry::Occupied(e) => e,
792 Entry::Vacant(e) => {
793 e.insert(Entrypoint::AppPage {
794 pages: vec![page],
795 loader_tree,
796 participating_page_files,
797 root_params,
798 });
799 return;
800 }
801 };
802
803 let conflict = |existing_name: &str, existing_page: &AppPage| {
804 conflict_issue(app_dir, &e, "page", existing_name, &page, existing_page);
805 };
806
807 let value = e.get();
808 match value {
809 Entrypoint::AppPage {
810 pages: existing_pages,
811 loader_tree: existing_loader_tree,
812 ..
813 } => {
814 if *existing_loader_tree != loader_tree {
817 conflict("page", existing_pages.first().unwrap());
818 }
819
820 let Entrypoint::AppPage {
821 pages: stored_pages,
822 participating_page_files: stored_page_files,
823 ..
824 } = e.get_mut()
825 else {
826 unreachable!("Entrypoint::AppPage was already matched");
827 };
828
829 stored_pages.push(page);
830 stored_pages.sort();
831 for page_file in participating_page_files {
832 if !stored_page_files.contains(&page_file) {
833 stored_page_files.push(page_file);
834 }
835 }
836 }
837 Entrypoint::AppRoute {
838 page: existing_page,
839 ..
840 } => {
841 conflict("route", existing_page);
842 }
843 Entrypoint::AppMetadata {
844 page: existing_page,
845 ..
846 } => {
847 conflict("metadata", existing_page);
848 }
849 }
850}
851
852fn add_app_route(
853 app_dir: FileSystemPath,
854 result: &mut FxIndexMap<AppPath, Entrypoint>,
855 page: AppPage,
856 path: FileSystemPath,
857 root_layouts: ResolvedVc<FileSystemPathVec>,
858 root_params: ResolvedVc<RootParamVecOption>,
859) {
860 let e = match result.entry(page.clone().into()) {
861 Entry::Occupied(e) => e,
862 Entry::Vacant(e) => {
863 e.insert(Entrypoint::AppRoute {
864 page,
865 path,
866 root_layouts,
867 root_params,
868 });
869 return;
870 }
871 };
872
873 let conflict = |existing_name: &str, existing_page: &AppPage| {
874 conflict_issue(app_dir, &e, "route", existing_name, &page, existing_page);
875 };
876
877 let value = e.get();
878 match value {
879 Entrypoint::AppPage { pages, .. } => {
880 conflict("page", pages.first().unwrap());
881 }
882 Entrypoint::AppRoute {
883 page: existing_page,
884 ..
885 } => {
886 conflict("route", existing_page);
887 }
888 Entrypoint::AppMetadata {
889 page: existing_page,
890 ..
891 } => {
892 conflict("metadata", existing_page);
893 }
894 }
895}
896
897fn add_app_metadata_route(
898 app_dir: FileSystemPath,
899 result: &mut FxIndexMap<AppPath, Entrypoint>,
900 page: AppPage,
901 metadata: MetadataItem,
902 root_params: ResolvedVc<RootParamVecOption>,
903) {
904 let e = match result.entry(page.clone().into()) {
905 Entry::Occupied(e) => e,
906 Entry::Vacant(e) => {
907 e.insert(Entrypoint::AppMetadata {
908 page,
909 metadata,
910 root_params,
911 });
912 return;
913 }
914 };
915
916 let conflict = |existing_name: &str, existing_page: &AppPage| {
917 conflict_issue(app_dir, &e, "metadata", existing_name, &page, existing_page);
918 };
919
920 let value = e.get();
921 match value {
922 Entrypoint::AppPage { pages, .. } => {
923 conflict("page", pages.first().unwrap());
924 }
925 Entrypoint::AppRoute {
926 page: existing_page,
927 ..
928 } => {
929 conflict("route", existing_page);
930 }
931 Entrypoint::AppMetadata {
932 page: existing_page,
933 ..
934 } => {
935 conflict("metadata", existing_page);
936 }
937 }
938}
939
940#[turbo_tasks::function]
941pub fn get_entrypoints(
942 app_dir: FileSystemPath,
943 page_extensions: Vc<Vec<RcStr>>,
944 is_global_not_found_enabled: Vc<bool>,
945 explicit_parallel_route_children: Vc<bool>,
946 strict_route_matching: Vc<bool>,
947 next_mode: Vc<NextMode>,
948) -> Vc<Entrypoints> {
949 directory_tree_to_entrypoints(
950 app_dir.clone(),
951 get_directory_tree(app_dir.clone(), page_extensions),
952 get_global_metadata(app_dir, page_extensions),
953 is_global_not_found_enabled,
954 explicit_parallel_route_children,
955 strict_route_matching,
956 next_mode,
957 Default::default(),
958 Default::default(),
959 )
960}
961
962#[turbo_tasks::value(transparent)]
963pub struct CollectedRootParams(#[bincode(with = "turbo_bincode::indexset")] FxIndexSet<RcStr>);
964
965#[turbo_tasks::function]
966pub async fn collect_root_params(
967 entrypoints: ResolvedVc<Entrypoints>,
968) -> Result<Vc<CollectedRootParams>> {
969 let mut collected_root_params = FxIndexSet::<RcStr>::default();
970 for (_, entrypoint) in entrypoints.await?.iter() {
971 if let Some(ref root_params) = *entrypoint.root_params().await? {
972 collected_root_params.extend(root_params.iter().cloned());
973 }
974 }
975 Ok(Vc::cell(collected_root_params))
976}
977
978#[turbo_tasks::function]
979async fn directory_tree_to_entrypoints(
980 app_dir: FileSystemPath,
981 directory_tree: Vc<DirectoryTree>,
982 global_metadata: Vc<GlobalMetadata>,
983 is_global_not_found_enabled: Vc<bool>,
984 explicit_parallel_route_children: Vc<bool>,
985 strict_route_matching: Vc<bool>,
986 next_mode: Vc<NextMode>,
987 root_layouts: Vc<FileSystemPathVec>,
988 root_params: Vc<RootParamVecOption>,
989) -> Result<Vc<Entrypoints>> {
990 let entrypoints = directory_tree_to_entrypoints_internal(
991 app_dir.clone(),
992 global_metadata,
993 is_global_not_found_enabled,
994 explicit_parallel_route_children,
995 strict_route_matching,
996 next_mode,
997 rcstr!(""),
998 directory_tree,
999 AppPage::new(),
1000 root_layouts,
1001 root_params,
1002 );
1003
1004 if !*strict_route_matching.await? {
1005 return Ok(entrypoints);
1006 }
1007
1008 let builtin_default = get_next_package(app_dir.clone())
1009 .await?
1010 .join("dist/client/components/builtin/default.js")?;
1011 let entrypoints_ref = entrypoints.await?;
1012 let plain_tree = directory_tree.into_plain().await?;
1013 let mut declared_slots = FxIndexMap::default();
1014 collect_declared_parallel_route_slots(&plain_tree, &mut declared_slots);
1015 let mut candidate_entrypoints = FxIndexMap::default();
1016
1017 for (app_path, entrypoint) in entrypoints_ref.iter() {
1023 let is_incomplete = match entrypoint {
1024 Entrypoint::AppPage { loader_tree, .. } => loader_tree
1025 .await?
1026 .has_unmatched_parallel_route(&builtin_default, &declared_slots, None),
1027 _ => false,
1028 };
1029
1030 if !is_incomplete {
1031 candidate_entrypoints.insert(app_path.clone(), entrypoint.clone());
1032 }
1033 }
1034
1035 let mut incompatible_parallel_route_slots = Vec::new();
1036 let mut retained_entrypoints = FxIndexMap::default();
1037
1038 for (app_path, entrypoint) in &candidate_entrypoints {
1042 let Entrypoint::AppPage { loader_tree, .. } = entrypoint else {
1043 retained_entrypoints.insert(app_path.clone(), entrypoint.clone());
1044 continue;
1045 };
1046 let loader_tree = loader_tree.await?;
1047 if !loader_tree.contains_declared_builtin_not_found_default(
1048 &builtin_default,
1049 &declared_slots,
1050 None,
1051 ) {
1052 retained_entrypoints.insert(app_path.clone(), entrypoint.clone());
1053 continue;
1054 }
1055 if app_path.intercepted_path().is_some() {
1056 continue;
1057 }
1058
1059 let mut missing_slots = FxIndexMap::default();
1060 loader_tree.collect_builtin_not_found_defaults(
1061 &builtin_default,
1062 &declared_slots,
1063 None,
1064 &mut missing_slots,
1065 );
1066 if missing_slots.is_empty() {
1067 bail!(
1068 "Invariant: strict route matching retained the incomplete route matcher \
1069 `{app_path}`"
1070 );
1071 }
1072 incompatible_parallel_route_slots.extend(
1073 missing_slots
1074 .into_iter()
1075 .map(|(layout, slots)| (layout, app_path.clone(), slots.into_iter().collect())),
1076 );
1077 }
1078
1079 if !incompatible_parallel_route_slots.is_empty() {
1080 IncompatibleParallelRouteSlotsIssue {
1081 app_dir: app_dir.clone(),
1082 routes: incompatible_parallel_route_slots,
1083 }
1084 .resolved_cell()
1085 .emit();
1086 }
1087
1088 for (app_path, entrypoint) in &retained_entrypoints {
1091 let Entrypoint::AppPage { loader_tree, .. } = entrypoint else {
1092 continue;
1093 };
1094 if !app_path.contains_interception()
1095 && loader_tree
1096 .await?
1097 .contains_declared_builtin_not_found_default(
1098 &builtin_default,
1099 &declared_slots,
1100 None,
1101 )
1102 {
1103 bail!(
1104 "Invariant: strict route matching retained the incomplete route matcher \
1105 `{app_path}`"
1106 );
1107 }
1108 }
1109
1110 let ordinary_routes = retained_entrypoints
1111 .iter()
1112 .filter_map(|(route, entrypoint)| {
1113 (!route.contains_interception() && matches!(entrypoint, Entrypoint::AppPage { .. }))
1114 .then_some(route)
1115 })
1116 .collect::<Vec<_>>();
1117 let missing_canonical_interception_routes = retained_entrypoints
1118 .iter()
1119 .filter_map(|(interception_route, entrypoint)| {
1120 let Entrypoint::AppPage {
1121 participating_page_files,
1122 ..
1123 } = entrypoint
1124 else {
1125 return None;
1126 };
1127 let canonical_route = interception_route.intercepted_path()?;
1128 if canonical_route.is_route_pattern_covered_by(ordinary_routes.iter().copied()) {
1129 return None;
1130 }
1131
1132 Some((
1133 interception_route.clone(),
1134 canonical_route,
1135 participating_page_files.first()?.clone(),
1136 ))
1137 })
1138 .collect::<Vec<_>>();
1139 if !missing_canonical_interception_routes.is_empty() {
1140 MissingCanonicalInterceptionRoutesIssue {
1141 routes: missing_canonical_interception_routes,
1142 }
1143 .resolved_cell()
1144 .emit();
1145 }
1146
1147 let mut authored_pages = FxIndexSet::default();
1148 collect_authored_page_files(&plain_tree, &mut authored_pages);
1149
1150 let mut matched_pages = FxIndexSet::default();
1151 for entrypoint in retained_entrypoints.values() {
1152 if let Entrypoint::AppPage {
1153 participating_page_files,
1154 ..
1155 } = entrypoint
1156 {
1157 matched_pages.extend(participating_page_files.iter().cloned());
1158 }
1159 }
1160
1161 let unmatched_pages = authored_pages
1162 .into_iter()
1163 .filter(|page| !matched_pages.contains(page))
1164 .collect::<Vec<_>>();
1165 if !unmatched_pages.is_empty() {
1166 UnmatchedAppPagesIssue {
1167 app_dir,
1168 pages: unmatched_pages,
1169 }
1170 .resolved_cell()
1171 .emit();
1172 }
1173 Ok(Vc::cell(retained_entrypoints))
1174}
1175
1176#[turbo_tasks::value]
1177struct MissingCanonicalInterceptionRoutesIssue {
1178 routes: Vec<(AppPath, AppPath, FileSystemPath)>,
1179}
1180
1181#[turbo_tasks::value]
1182struct IncompatibleParallelRouteSlotsIssue {
1183 app_dir: FileSystemPath,
1184 routes: Vec<(FileSystemPath, AppPath, Vec<RcStr>)>,
1185}
1186
1187#[async_trait]
1188#[turbo_tasks::value_impl]
1189impl Issue for IncompatibleParallelRouteSlotsIssue {
1190 async fn file_path(&self) -> Result<FileSystemPath> {
1191 Ok(self.routes[0].0.clone())
1192 }
1193
1194 fn stage(&self) -> IssueStage {
1195 IssueStage::AppStructure
1196 }
1197
1198 fn severity(&self) -> IssueSeverity {
1199 IssueSeverity::Error
1200 }
1201
1202 async fn title(&self) -> Result<StyledString> {
1203 Ok(StyledString::Text(rcstr!(
1204 "Parallel route slots cannot render the same URLs"
1205 )))
1206 }
1207
1208 async fn description(&self) -> Result<Option<StyledString>> {
1209 let mut routes_by_layout = FxIndexMap::<FileSystemPath, Vec<_>>::default();
1210 for (layout, route, missing_slots) in &self.routes {
1211 routes_by_layout
1212 .entry(layout.clone())
1213 .or_default()
1214 .push((route.clone(), missing_slots.clone()));
1215 }
1216
1217 let mut layouts = routes_by_layout
1218 .into_iter()
1219 .map(|(layout, mut routes)| {
1220 routes.sort_by_cached_key(|(route, _)| route.to_string());
1221 let layout_path = self
1222 .app_dir
1223 .get_path_to(&layout)
1224 .expect("parallel route layout should be within the app directory")
1225 .to_string();
1226 let routes = routes
1227 .into_iter()
1228 .map(|(route, missing_slots)| {
1229 let missing_slots = missing_slots
1230 .iter()
1231 .map(|slot| {
1232 if &**slot == "children" {
1233 slot.to_string()
1234 } else {
1235 format!("@{slot}")
1236 }
1237 })
1238 .collect::<Vec<_>>()
1239 .join(", ");
1240 format!(
1241 "- {route} is missing a matching page or default.tsx in \
1242 {missing_slots}"
1243 )
1244 })
1245 .collect::<Vec<_>>()
1246 .join("\n");
1247 (layout_path, routes)
1248 })
1249 .collect::<Vec<_>>();
1250 layouts.sort_by(|a, b| a.0.cmp(&b.0));
1251 let layouts = layouts
1252 .into_iter()
1253 .map(|(layout_path, routes)| format!("app/{layout_path}\n{routes}"))
1254 .collect::<Vec<_>>()
1255 .join("\n\n");
1256
1257 Ok(Some(StyledString::Text(
1258 format!(
1259 "The following layouts have parallel route slots that cannot render the same \
1260 URLs:\n{layouts}\n\nEvery URL matched by one slot must have a matching page or \
1261 default.tsx in every sibling slot."
1262 )
1263 .into(),
1264 )))
1265 }
1266}
1267
1268#[async_trait]
1269#[turbo_tasks::value_impl]
1270impl Issue for MissingCanonicalInterceptionRoutesIssue {
1271 async fn file_path(&self) -> Result<FileSystemPath> {
1272 Ok(self.routes[0].2.clone())
1273 }
1274
1275 fn stage(&self) -> IssueStage {
1276 IssueStage::AppStructure
1277 }
1278
1279 fn severity(&self) -> IssueSeverity {
1280 IssueSeverity::Error
1281 }
1282
1283 async fn title(&self) -> Result<StyledString> {
1284 Ok(StyledString::Text(rcstr!(
1285 "Interception routes must have a canonical route"
1286 )))
1287 }
1288
1289 async fn description(&self) -> Result<Option<StyledString>> {
1290 let routes = self
1291 .routes
1292 .iter()
1293 .map(|(interception_route, canonical_route, _)| {
1294 format!("- {interception_route} (expected {canonical_route})")
1295 })
1296 .collect::<Vec<_>>()
1297 .join("\n");
1298 Ok(Some(StyledString::Text(
1299 format!(
1300 "The following interception routes do not have a canonical \
1301 route:\n{routes}\n\nEvery interception route must have a matching \
1302 non-interception route so the URL can be loaded directly or refreshed."
1303 )
1304 .into(),
1305 )))
1306 }
1307}
1308
1309fn collect_authored_page_files(
1310 directory_tree: &PlainDirectoryTree,
1311 pages: &mut FxIndexSet<FileSystemPath>,
1312) {
1313 if let Some(page) = &directory_tree.modules.page {
1314 pages.insert(page.clone());
1315 }
1316
1317 for subdirectory in directory_tree.subdirectories.values() {
1318 collect_authored_page_files(subdirectory, pages);
1319 }
1320}
1321
1322#[turbo_tasks::value]
1323struct UnmatchedAppPagesIssue {
1324 app_dir: FileSystemPath,
1325 pages: Vec<FileSystemPath>,
1326}
1327
1328#[async_trait]
1329#[turbo_tasks::value_impl]
1330impl Issue for UnmatchedAppPagesIssue {
1331 async fn file_path(&self) -> Result<FileSystemPath> {
1332 Ok(self.pages[0].clone())
1333 }
1334
1335 fn stage(&self) -> IssueStage {
1336 IssueStage::AppStructure
1337 }
1338
1339 fn severity(&self) -> IssueSeverity {
1340 IssueSeverity::Error
1341 }
1342
1343 async fn title(&self) -> Result<StyledString> {
1344 Ok(StyledString::Text(rcstr!("Unmatched app pages")))
1345 }
1346
1347 async fn description(&self) -> Result<Option<StyledString>> {
1348 let page_paths = self
1349 .pages
1350 .iter()
1351 .map(|page| {
1352 let relative_path = self
1353 .app_dir
1354 .get_path_to(page)
1355 .expect("authored page should be within the app directory");
1356 format!("- app/{}", relative_path)
1357 })
1358 .collect::<Vec<_>>()
1359 .join("\n");
1360 Ok(Some(StyledString::Text(
1361 format!(
1362 "The following page files do not match any complete route:\n{page_paths}\n\nEvery \
1363 page must be part of at least one complete route. Add matching pages or default \
1364 files for the sibling parallel route slots, or remove the unreachable pages."
1365 )
1366 .into(),
1367 )))
1368 }
1369}
1370
1371#[turbo_tasks::value]
1372struct DuplicateParallelRouteIssue {
1373 app_dir: FileSystemPath,
1374 previously_inserted_page: AppPage,
1375 page: AppPage,
1376}
1377
1378#[async_trait]
1379#[turbo_tasks::value_impl]
1380impl Issue for DuplicateParallelRouteIssue {
1381 async fn file_path(&self) -> Result<FileSystemPath> {
1382 self.app_dir.join(&self.page.to_string())
1383 }
1384
1385 fn stage(&self) -> IssueStage {
1386 IssueStage::ProcessModule
1387 }
1388
1389 async fn title(&self) -> Result<StyledString> {
1390 Ok(StyledString::Text(
1391 format!(
1392 "You cannot have two parallel pages that resolve to the same path. Please check \
1393 {} and {}.",
1394 self.previously_inserted_page, self.page
1395 )
1396 .into(),
1397 ))
1398 }
1399}
1400
1401#[turbo_tasks::value]
1402struct MissingRootLayoutIssue {
1403 app_dir: FileSystemPath,
1404 page_path: FileSystemPath,
1405}
1406
1407#[async_trait]
1408#[turbo_tasks::value_impl]
1409impl Issue for MissingRootLayoutIssue {
1410 async fn file_path(&self) -> Result<FileSystemPath> {
1411 Ok(self.page_path.clone())
1412 }
1413
1414 fn stage(&self) -> IssueStage {
1415 IssueStage::AppStructure
1416 }
1417
1418 fn severity(&self) -> IssueSeverity {
1419 IssueSeverity::Error
1420 }
1421
1422 async fn title(&self) -> Result<StyledString> {
1423 let page_path = self
1424 .app_dir
1425 .get_path_to(&self.page_path)
1426 .context("page should be within the app directory")?;
1427
1428 Ok(StyledString::Text(
1429 format!(
1430 "{page_path} doesn't have a root layout. To fix this error, make sure every page \
1431 has a root layout."
1432 )
1433 .into(),
1434 ))
1435 }
1436}
1437
1438#[turbo_tasks::value]
1439struct MissingDefaultParallelRouteIssue {
1440 app_dir: FileSystemPath,
1441 app_page: AppPage,
1442 slot_name: RcStr,
1443}
1444
1445#[turbo_tasks::function]
1446fn missing_default_parallel_route_issue(
1447 app_dir: FileSystemPath,
1448 app_page: AppPage,
1449 slot_name: RcStr,
1450) -> Vc<MissingDefaultParallelRouteIssue> {
1451 MissingDefaultParallelRouteIssue {
1452 app_dir,
1453 app_page,
1454 slot_name,
1455 }
1456 .cell()
1457}
1458
1459#[async_trait]
1460#[turbo_tasks::value_impl]
1461impl Issue for MissingDefaultParallelRouteIssue {
1462 async fn file_path(&self) -> Result<FileSystemPath> {
1463 self.app_dir
1464 .join(&self.app_page.to_string())?
1465 .join(&format!("@{}", self.slot_name))
1466 }
1467
1468 fn stage(&self) -> IssueStage {
1469 IssueStage::AppStructure
1470 }
1471
1472 fn severity(&self) -> IssueSeverity {
1473 IssueSeverity::Error
1474 }
1475
1476 async fn title(&self) -> Result<StyledString> {
1477 Ok(StyledString::Text(
1478 format!(
1479 "Missing required default.js file for parallel route at {}/@{}",
1480 self.app_page, self.slot_name
1481 )
1482 .into(),
1483 ))
1484 }
1485
1486 async fn description(&self) -> Result<Option<StyledString>> {
1487 Ok(Some(StyledString::Stack(vec![
1488 StyledString::Text(
1489 format!(
1490 "The parallel route slot \"@{}\" is missing a default.js file. When using \
1491 parallel routes, each slot must have a default.js file to serve as a \
1492 fallback.",
1493 self.slot_name
1494 )
1495 .into(),
1496 ),
1497 StyledString::Text(
1498 format!(
1499 "Create a default.js file at: {}/@{}/default.js",
1500 self.app_page, self.slot_name
1501 )
1502 .into(),
1503 ),
1504 ])))
1505 }
1506
1507 fn documentation_link(&self) -> RcStr {
1508 rcstr!("https://nextjs.org/docs/messages/slot-missing-default")
1509 }
1510}
1511
1512fn page_path_except_parallel(loader_tree: &AppPageLoaderTree) -> Option<AppPage> {
1513 if loader_tree.page.iter().any(|v| {
1514 matches!(
1515 v,
1516 PageSegment::CatchAll(..)
1517 | PageSegment::OptionalCatchAll(..)
1518 | PageSegment::Parallel(..)
1519 )
1520 }) {
1521 return None;
1522 }
1523
1524 if loader_tree.modules.page.is_some() {
1525 return Some(loader_tree.page.clone());
1526 }
1527
1528 if let Some(children) = loader_tree.parallel_routes.get("children") {
1529 return page_path_except_parallel(children);
1530 }
1531
1532 None
1533}
1534
1535fn has_child_routes(directory_tree: &PlainDirectoryTree) -> bool {
1539 for (name, subdirectory) in &directory_tree.subdirectories {
1540 if is_parallel_route(name) {
1542 continue;
1543 }
1544
1545 if is_group_route(name) {
1547 if has_child_routes(subdirectory) {
1549 return true;
1550 }
1551 continue;
1552 }
1553
1554 return true;
1556 }
1557
1558 false
1559}
1560
1561fn has_declared_children_slot(directory_tree: &PlainDirectoryTree) -> bool {
1565 directory_tree.modules.page.is_some()
1566 || directory_tree.modules.default.is_some()
1567 || directory_tree
1568 .subdirectories
1569 .iter()
1570 .filter(|(name, _)| !is_parallel_route(name))
1571 .any(|(_, subdirectory)| subdirectory.contains_page_or_default)
1572}
1573
1574fn collect_named_slots_at_level(
1577 directory_tree: &PlainDirectoryTree,
1578 slots: &mut FxIndexSet<RcStr>,
1579) {
1580 for (name, subdirectory) in &directory_tree.subdirectories {
1581 if let Some(slot) = match_parallel_route(name) {
1582 if subdirectory.contains_page_or_default {
1583 slots.insert(slot.into());
1584 }
1585 } else if is_group_route(name) {
1586 collect_named_slots_at_level(subdirectory, slots);
1587 }
1588 }
1589}
1590
1591fn collect_declared_parallel_route_slots(
1595 directory_tree: &PlainDirectoryTree,
1596 slots_by_layout: &mut FxIndexMap<FileSystemPath, FxIndexSet<RcStr>>,
1597) {
1598 if let Some(layout) = &directory_tree.modules.layout {
1599 let mut slots = FxIndexSet::default();
1600 if has_declared_children_slot(directory_tree) {
1601 slots.insert(rcstr!("children"));
1602 }
1603 collect_named_slots_at_level(directory_tree, &mut slots);
1604 slots_by_layout.insert(layout.clone(), slots);
1605 }
1606
1607 for subdirectory in directory_tree.subdirectories.values() {
1608 collect_declared_parallel_route_slots(subdirectory, slots_by_layout);
1609 }
1610}
1611
1612async fn check_duplicate(
1613 duplicate: &mut FxHashMap<AppPath, AppPage>,
1614 loader_tree: &AppPageLoaderTree,
1615 app_dir: FileSystemPath,
1616) -> Result<()> {
1617 let page_path = page_path_except_parallel(loader_tree);
1618
1619 if let Some(page_path) = page_path
1620 && let Some(prev) = duplicate.insert(AppPath::from(page_path.clone()), page_path.clone())
1621 && prev != page_path
1622 {
1623 DuplicateParallelRouteIssue {
1624 app_dir: app_dir.clone(),
1625 previously_inserted_page: prev.clone(),
1626 page: loader_tree.page.clone(),
1627 }
1628 .resolved_cell()
1629 .emit();
1630 }
1631
1632 Ok(())
1633}
1634
1635#[turbo_tasks::value(transparent)]
1636struct AppPageLoaderTreeOption(Option<ResolvedVc<AppPageLoaderTree>>);
1637
1638#[turbo_tasks::function]
1640async fn directory_tree_to_loader_tree(
1641 app_dir: FileSystemPath,
1642 global_metadata: Vc<GlobalMetadata>,
1643 directory_name: RcStr,
1644 directory_tree: Vc<DirectoryTree>,
1645 app_page: AppPage,
1646 for_app_path: AppPath,
1648 explicit_parallel_route_children: Vc<bool>,
1649 strict_route_matching: Vc<bool>,
1650) -> Result<Vc<AppPageLoaderTreeOption>> {
1651 let plain_tree_vc = directory_tree.into_plain();
1652 let plain_tree = &*plain_tree_vc.await?;
1653 let strict_route_matching = *strict_route_matching.await?;
1654 let mut missing_defaults = Vec::new();
1655 let tree = directory_tree_to_loader_tree_internal(
1656 app_dir.clone(),
1657 global_metadata,
1658 directory_name,
1659 plain_tree,
1660 app_page.clone(),
1661 for_app_path,
1662 *explicit_parallel_route_children.await?,
1663 AppDirModules::default(),
1664 Some(&plain_tree.url_tree),
1665 &mut missing_defaults,
1666 )
1667 .await?;
1668
1669 if !strict_route_matching {
1672 for (page, slot) in missing_defaults {
1673 missing_default_parallel_route_issue(app_dir.clone(), page, slot)
1674 .to_resolved()
1675 .await?
1676 .emit();
1677 }
1678 }
1679
1680 Ok(Vc::cell(tree.map(AppPageLoaderTree::resolved_cell)))
1681}
1682
1683async fn check_and_update_module_references(
1698 app_dir: FileSystemPath,
1699 module: &mut Option<FileSystemPath>,
1700 parent_module: &mut Option<FileSystemPath>,
1701 file_path: &str,
1702 is_first_layer_group_route: bool,
1703) -> Result<()> {
1704 match (module.as_mut(), parent_module.as_mut()) {
1705 (Some(module), _) => *parent_module = Some(module.clone()),
1707 (None, Some(parent_module)) if is_first_layer_group_route => {
1710 *module = Some(parent_module.clone())
1711 }
1712 (None, Some(_)) => {}
1715 (None, None) => {
1719 let default_page = get_next_package(app_dir).await?.join(file_path)?;
1720 *module = Some(default_page.clone());
1721 *parent_module = Some(default_page);
1722 }
1723 }
1724
1725 Ok(())
1726}
1727
1728async fn check_and_update_global_module_references(
1736 app_dir: FileSystemPath,
1737 module: &mut Option<FileSystemPath>,
1738 file_path: &str,
1739) -> Result<()> {
1740 if module.is_none() {
1741 *module = Some(get_next_package(app_dir).await?.join(file_path)?);
1742 }
1743
1744 Ok(())
1745}
1746
1747async fn directory_tree_to_loader_tree_internal(
1748 app_dir: FileSystemPath,
1749 global_metadata: Vc<GlobalMetadata>,
1750 directory_name: RcStr,
1751 directory_tree: &PlainDirectoryTree,
1752 app_page: AppPage,
1753 for_app_path: AppPath,
1755 explicit_parallel_route_children: bool,
1756 mut parent_modules: AppDirModules,
1757 url_tree: Option<&UrlSegmentTree>,
1758 missing_defaults: &mut Vec<(AppPage, RcStr)>,
1759) -> Result<Option<AppPageLoaderTree>> {
1760 let app_path = AppPath::from(app_page.clone());
1761
1762 if !for_app_path.contains(&app_path) {
1763 return Ok(None);
1764 }
1765
1766 let mut modules = directory_tree.modules.clone();
1767
1768 modules.metadata.base_page = Some(app_page.clone());
1771
1772 let is_root_directory = app_page.is_root();
1774
1775 let is_first_layer_group_route = app_page.is_first_layer_group_route();
1777
1778 if is_root_directory || is_first_layer_group_route {
1781 check_and_update_module_references(
1782 app_dir.clone(),
1783 &mut modules.not_found,
1784 &mut parent_modules.not_found,
1785 "dist/client/components/builtin/not-found.js",
1786 is_first_layer_group_route,
1787 )
1788 .await?;
1789
1790 check_and_update_module_references(
1791 app_dir.clone(),
1792 &mut modules.forbidden,
1793 &mut parent_modules.forbidden,
1794 "dist/client/components/builtin/forbidden.js",
1795 is_first_layer_group_route,
1796 )
1797 .await?;
1798
1799 check_and_update_module_references(
1800 app_dir.clone(),
1801 &mut modules.unauthorized,
1802 &mut parent_modules.unauthorized,
1803 "dist/client/components/builtin/unauthorized.js",
1804 is_first_layer_group_route,
1805 )
1806 .await?;
1807 }
1808
1809 if is_root_directory {
1810 check_and_update_global_module_references(
1811 app_dir.clone(),
1812 &mut modules.global_error,
1813 "dist/client/components/builtin/global-error.js",
1814 )
1815 .await?;
1816 }
1817
1818 let static_siblings: Vec<RcStr> = if is_dynamic_segment(&directory_name) {
1822 url_tree
1823 .map(|t| {
1824 t.static_children()
1825 .into_iter()
1826 .filter(|s| s != &directory_name)
1827 .collect()
1828 })
1829 .unwrap_or_default()
1830 } else {
1831 Vec::new()
1833 };
1834
1835 let mut tree = AppPageLoaderTree {
1836 page: app_page.clone(),
1837 segment: directory_name.clone(),
1838 parallel_routes: FxIndexMap::default(),
1839 modules: modules.without_leaves(),
1840 global_metadata: global_metadata.to_resolved().await?,
1841 static_siblings,
1842 };
1843
1844 let current_level_is_parallel_route = is_parallel_route(&directory_name);
1845
1846 if current_level_is_parallel_route {
1847 tree.segment = rcstr!("(__SLOT__)");
1848 }
1849
1850 if let Some(page) = (app_path == for_app_path || app_path.is_catchall())
1851 .then_some(modules.page)
1852 .flatten()
1853 {
1854 tree.parallel_routes.insert(
1855 rcstr!("children"),
1856 AppPageLoaderTree {
1857 page: app_page.clone(),
1858 segment: rcstr!("__PAGE__"),
1859 parallel_routes: FxIndexMap::default(),
1860 modules: AppDirModules {
1861 page: Some(page),
1862 metadata: modules.metadata,
1863 ..Default::default()
1864 },
1865 global_metadata: global_metadata.to_resolved().await?,
1866 static_siblings: Vec::new(),
1867 },
1868 );
1869 }
1870
1871 let mut duplicate = FxHashMap::default();
1872
1873 for (subdir_name, subdirectory) in &directory_tree.subdirectories {
1874 let parallel_route_key = match_parallel_route(subdir_name);
1875
1876 let mut child_app_page = app_page.clone();
1877 let mut illegal_path_error = None;
1878
1879 if let Err(e) = child_app_page.push_str(&normalize_underscore(subdir_name)) {
1883 illegal_path_error = Some(e);
1884 }
1885
1886 let child_url_tree: Option<&UrlSegmentTree> =
1888 if directory_name.is_empty() || is_url_transparent_segment(&directory_name) {
1889 url_tree
1890 } else {
1891 url_tree.and_then(|t| t.get_child(&directory_name))
1892 };
1893
1894 let subtree = Box::pin(directory_tree_to_loader_tree_internal(
1895 app_dir.clone(),
1896 global_metadata,
1897 subdir_name.clone(),
1898 subdirectory,
1899 child_app_page.clone(),
1900 for_app_path.clone(),
1901 explicit_parallel_route_children,
1902 parent_modules.clone(),
1903 child_url_tree,
1904 missing_defaults,
1905 ))
1906 .await?;
1907
1908 if let Some(illegal_path) = subtree.as_ref().and(illegal_path_error) {
1909 return Err(illegal_path);
1910 }
1911
1912 if let Some(subtree) = subtree {
1913 if let Some(key) = parallel_route_key {
1914 let is_inside_catchall = app_page.is_catchall();
1925
1926 let is_leaf_segment = !has_child_routes(directory_tree);
1934
1935 let slot_has_children = has_child_routes(subdirectory);
1946
1947 if key != "children"
1948 && subdirectory.modules.default.is_none()
1949 && !is_inside_catchall
1950 && !is_leaf_segment
1951 && !slot_has_children
1952 {
1953 missing_defaults.push((app_page.clone(), key.into()));
1954 }
1955
1956 tree.parallel_routes.insert(key.into(), subtree);
1957 continue;
1958 }
1959
1960 if is_group_route(subdir_name) && !subtree.has_page() {
1962 continue;
1963 }
1964
1965 if subtree.has_page() {
1966 check_duplicate(&mut duplicate, &subtree, app_dir.clone()).await?;
1967 }
1968
1969 if let Some(current_tree) = tree.parallel_routes.get("children") {
1970 if current_tree.has_only_catchall()
1971 && (!subtree.has_only_catchall()
1972 || current_tree.get_specificity() < subtree.get_specificity())
1973 {
1974 tree.parallel_routes
1975 .insert(rcstr!("children"), subtree.clone());
1976 }
1977 } else {
1978 tree.parallel_routes.insert(rcstr!("children"), subtree);
1979 }
1980 } else if let Some(key) = parallel_route_key {
1981 bail!(
1982 "missing page or default for parallel route `{}` (page: {})",
1983 key,
1984 app_page
1985 );
1986 }
1987 }
1988
1989 let is_interception_host = !app_path.contains_interception()
1993 && tree
1994 .parallel_routes
1995 .iter()
1996 .any(|(_, parallel_tree)| parallel_tree.is_intercepting());
1997
1998 if is_interception_host {
1999 let keys_to_replace = tree
2000 .parallel_routes
2001 .iter()
2002 .filter(|(_, parallel_tree)| !parallel_tree.is_intercepting())
2003 .map(|(key, _)| key.clone())
2004 .collect::<Vec<_>>();
2005 let is_inside_catchall = app_page.is_catchall();
2006 let is_leaf_segment = !has_child_routes(directory_tree);
2007
2008 for key in keys_to_replace {
2009 let subdir_name: RcStr = format!("@{key}").into();
2010
2011 let default = if key == "children" {
2012 modules.default.clone()
2013 } else if let Some(subdirectory) = directory_tree.subdirectories.get(&subdir_name) {
2014 subdirectory.modules.default.clone()
2015 } else {
2016 None
2017 };
2018 let slot_has_children = directory_tree
2019 .subdirectories
2020 .get(&subdir_name)
2021 .is_some_and(has_child_routes);
2022
2023 if default.is_none()
2028 && key != "children"
2029 && !is_inside_catchall
2030 && !is_leaf_segment
2031 && !slot_has_children
2032 {
2033 missing_defaults.push((app_page.clone(), key.clone()));
2034 }
2035
2036 tree.parallel_routes.insert(
2037 key.clone(),
2038 retained_route_tree(app_dir.clone(), global_metadata, app_page.clone()).await?,
2039 );
2040 }
2041 }
2042
2043 if tree.parallel_routes.is_empty() {
2044 if modules.default.is_some() || current_level_is_parallel_route {
2045 tree = default_route_tree(
2046 app_dir.clone(),
2047 global_metadata,
2048 app_page.clone(),
2049 modules.default.clone(),
2050 rcstr!("children"),
2051 for_app_path.clone(),
2052 )
2053 .await?;
2054 } else {
2055 return Ok(None);
2056 }
2057 } else if tree.parallel_routes.get("children").is_none()
2058 && (!explicit_parallel_route_children || has_declared_children_slot(directory_tree))
2059 {
2060 let children = if is_interception_host {
2064 retained_route_tree(app_dir.clone(), global_metadata, app_page.clone()).await?
2065 } else {
2066 default_route_tree(
2067 app_dir.clone(),
2068 global_metadata,
2069 app_page.clone(),
2070 modules.default.clone(),
2071 rcstr!("children"),
2072 for_app_path.clone(),
2073 )
2074 .await?
2075 };
2076 tree.parallel_routes.insert(rcstr!("children"), children);
2077 }
2078
2079 Ok(Some(tree))
2080}
2081
2082async fn default_route_tree(
2083 app_dir: FileSystemPath,
2084 global_metadata: Vc<GlobalMetadata>,
2085 app_page: AppPage,
2086 default_component: Option<FileSystemPath>,
2087 slot_name: RcStr,
2088 for_app_path: AppPath,
2089) -> Result<AppPageLoaderTree> {
2090 let default = if let Some(default) = default_component {
2091 default
2092 } else {
2093 let contains_interception = for_app_path.contains_interception();
2094
2095 let default_file = if contains_interception && slot_name == "children" {
2100 "dist/client/components/builtin/default-null.js"
2101 } else {
2102 "dist/client/components/builtin/default.js"
2103 };
2104
2105 get_next_package(app_dir).await?.join(default_file)?
2106 };
2107
2108 synthetic_default_route_tree(global_metadata, app_page, default).await
2109}
2110
2111async fn retained_route_tree(
2112 app_dir: FileSystemPath,
2113 global_metadata: Vc<GlobalMetadata>,
2114 app_page: AppPage,
2115) -> Result<AppPageLoaderTree> {
2116 let default_null = get_next_package(app_dir)
2117 .await?
2118 .join("dist/client/components/builtin/default-null.js")?;
2119 synthetic_default_route_tree(global_metadata, app_page, default_null).await
2120}
2121
2122async fn synthetic_default_route_tree(
2123 global_metadata: Vc<GlobalMetadata>,
2124 app_page: AppPage,
2125 default: FileSystemPath,
2126) -> Result<AppPageLoaderTree> {
2127 Ok(AppPageLoaderTree {
2128 page: app_page,
2129 segment: rcstr!("__DEFAULT__"),
2130 parallel_routes: FxIndexMap::default(),
2131 modules: AppDirModules {
2132 default: Some(default),
2133 ..Default::default()
2134 },
2135 global_metadata: global_metadata.to_resolved().await?,
2136 static_siblings: Vec::new(),
2137 })
2138}
2139
2140#[turbo_tasks::function]
2141async fn directory_tree_to_entrypoints_internal(
2142 app_dir: FileSystemPath,
2143 global_metadata: ResolvedVc<GlobalMetadata>,
2144 is_global_not_found_enabled: Vc<bool>,
2145 explicit_parallel_route_children: Vc<bool>,
2146 strict_route_matching: Vc<bool>,
2147 next_mode: Vc<NextMode>,
2148 directory_name: RcStr,
2149 directory_tree: Vc<DirectoryTree>,
2150 app_page: AppPage,
2151 root_layouts: ResolvedVc<FileSystemPathVec>,
2152 root_params: ResolvedVc<RootParamVecOption>,
2153) -> Result<Vc<Entrypoints>> {
2154 let span = tracing::info_span!("build layout trees", name = display(&app_page));
2155 directory_tree_to_entrypoints_internal_untraced(
2156 app_dir,
2157 global_metadata,
2158 is_global_not_found_enabled,
2159 explicit_parallel_route_children,
2160 strict_route_matching,
2161 next_mode,
2162 directory_name,
2163 directory_tree,
2164 app_page,
2165 root_layouts,
2166 root_params,
2167 )
2168 .instrument(span)
2169 .await
2170}
2171
2172async fn directory_tree_to_entrypoints_internal_untraced(
2173 app_dir: FileSystemPath,
2174 global_metadata: ResolvedVc<GlobalMetadata>,
2175 is_global_not_found_enabled: Vc<bool>,
2176 explicit_parallel_route_children: Vc<bool>,
2177 strict_route_matching: Vc<bool>,
2178 next_mode: Vc<NextMode>,
2179 directory_name: RcStr,
2180 directory_tree: Vc<DirectoryTree>,
2181 app_page: AppPage,
2182 root_layouts: ResolvedVc<FileSystemPathVec>,
2183 root_params: ResolvedVc<RootParamVecOption>,
2184) -> Result<Vc<Entrypoints>> {
2185 let mut result = FxIndexMap::default();
2186
2187 let directory_tree_vc = directory_tree;
2188 let directory_tree = &*directory_tree.await?;
2189
2190 let subdirectories = &directory_tree.subdirectories;
2191 let modules = &directory_tree.modules;
2192 let root_layouts = if let Some(layout) = &modules.layout {
2196 let mut layouts = root_layouts.owned().await?;
2197 layouts.push(layout.clone());
2198 ResolvedVc::cell(layouts)
2199 } else {
2200 root_layouts
2201 };
2202
2203 let root_params = if root_params.await?.is_none() && (*root_layouts.await?).len() == 1 {
2205 ResolvedVc::cell(Some(
2208 app_page
2209 .0
2210 .iter()
2211 .filter_map(|segment| match segment {
2212 PageSegment::Dynamic(param)
2213 | PageSegment::CatchAll(param)
2214 | PageSegment::OptionalCatchAll(param) => Some(param.clone()),
2215 _ => None,
2216 })
2217 .collect::<Vec<RcStr>>(),
2218 ))
2219 } else {
2220 root_params
2221 };
2222
2223 if let Some(page_path) = &modules.page {
2224 if root_layouts.await?.is_empty() {
2225 MissingRootLayoutIssue {
2226 app_dir: app_dir.clone(),
2227 page_path: page_path.clone(),
2228 }
2229 .resolved_cell()
2230 .emit();
2231 }
2232
2233 let app_path = AppPath::from(app_page.clone());
2234
2235 let loader_tree = *directory_tree_to_loader_tree(
2236 app_dir.clone(),
2237 *global_metadata,
2238 directory_name.clone(),
2239 directory_tree_vc,
2240 app_page.clone(),
2241 app_path,
2242 explicit_parallel_route_children,
2243 strict_route_matching,
2244 )
2245 .await?;
2246
2247 let loader_tree =
2248 loader_tree.context("loader tree should be created for a page/default")?;
2249 let mut participating_page_files = FxIndexSet::default();
2250 loader_tree
2251 .await?
2252 .collect_page_files(&mut participating_page_files);
2253 add_app_page(
2254 app_dir.clone(),
2255 &mut result,
2256 app_page.complete(PageType::Page)?,
2257 loader_tree,
2258 participating_page_files.into_iter().collect(),
2259 root_params,
2260 );
2261 }
2262
2263 if let Some(route) = &modules.route {
2264 add_app_route(
2265 app_dir.clone(),
2266 &mut result,
2267 app_page.complete(PageType::Route)?,
2268 route.clone(),
2269 root_layouts,
2270 root_params,
2271 );
2272 }
2273
2274 let Metadata {
2275 icon,
2276 apple,
2277 twitter,
2278 open_graph,
2279 sitemap,
2280 base_page: _,
2281 } = &modules.metadata;
2282
2283 for meta in sitemap
2284 .iter()
2285 .cloned()
2286 .chain(icon.iter().cloned().map(MetadataItem::from))
2287 .chain(apple.iter().cloned().map(MetadataItem::from))
2288 .chain(twitter.iter().cloned().map(MetadataItem::from))
2289 .chain(open_graph.iter().cloned().map(MetadataItem::from))
2290 {
2291 let app_page = app_page.clone_push_str(&get_metadata_route_name(meta.clone()).await?)?;
2292
2293 add_app_metadata_route(
2294 app_dir.clone(),
2295 &mut result,
2296 normalize_metadata_route(app_page)?,
2297 meta,
2298 root_params,
2299 );
2300 }
2301
2302 if app_page.is_root() {
2304 let GlobalMetadata {
2305 favicon,
2306 robots,
2307 manifest,
2308 } = &*global_metadata.await?;
2309
2310 for meta in favicon.iter().chain(robots.iter()).chain(manifest.iter()) {
2311 let app_page =
2312 app_page.clone_push_str(&get_metadata_route_name(meta.clone()).await?)?;
2313
2314 add_app_metadata_route(
2315 app_dir.clone(),
2316 &mut result,
2317 normalize_metadata_route(app_page)?,
2318 meta.clone(),
2319 root_params,
2320 );
2321 }
2322
2323 let mut modules = directory_tree.modules.clone();
2324
2325 if modules.layout.is_none() {
2327 modules.layout = Some(
2328 get_next_package(app_dir.clone())
2329 .await?
2330 .join("dist/client/components/builtin/layout.js")?,
2331 );
2332 }
2333
2334 if modules.not_found.is_none() {
2335 modules.not_found = Some(
2336 get_next_package(app_dir.clone())
2337 .await?
2338 .join("dist/client/components/builtin/not-found.js")?,
2339 );
2340 }
2341 if modules.forbidden.is_none() {
2342 modules.forbidden = Some(
2343 get_next_package(app_dir.clone())
2344 .await?
2345 .join("dist/client/components/builtin/forbidden.js")?,
2346 );
2347 }
2348 if modules.unauthorized.is_none() {
2349 modules.unauthorized = Some(
2350 get_next_package(app_dir.clone())
2351 .await?
2352 .join("dist/client/components/builtin/unauthorized.js")?,
2353 );
2354 }
2355 if modules.global_error.is_none() {
2356 modules.global_error = Some(
2357 get_next_package(app_dir.clone())
2358 .await?
2359 .join("dist/client/components/builtin/global-error.js")?,
2360 );
2361 }
2362
2363 let is_global_not_found_enabled = *is_global_not_found_enabled.await?;
2368 let use_global_not_found =
2369 is_global_not_found_enabled || modules.global_not_found.is_some();
2370
2371 let not_found_root_modules = modules.without_leaves();
2372 let not_found_tree = AppPageLoaderTree {
2373 page: app_page.clone(),
2374 segment: directory_name.clone(),
2375 parallel_routes: fxindexmap! {
2376 rcstr!("children") => AppPageLoaderTree {
2377 page: app_page.clone(),
2378 segment: rcstr!("/_not-found"),
2379 parallel_routes: fxindexmap! {
2380 rcstr!("children") => AppPageLoaderTree {
2381 page: app_page.clone(),
2382 segment: rcstr!("__PAGE__"),
2383 parallel_routes: FxIndexMap::default(),
2384 modules: if use_global_not_found {
2385 AppDirModules {
2388 page: Some(get_next_package(app_dir.clone())
2390 .await?
2391 .join("dist/client/components/builtin/empty-stub.js")?,
2392 ),
2393 ..Default::default()
2394 }
2395 } else {
2396 AppDirModules {
2399 page: match modules.not_found {
2400 Some(v) => Some(v),
2401 None => Some(get_next_package(app_dir.clone())
2402 .await?
2403 .join("dist/client/components/builtin/not-found.js")?,
2404 ),
2405 },
2406 ..Default::default()
2407 }
2408 },
2409 global_metadata,
2410 static_siblings: Vec::new(),
2411 }
2412 },
2413 modules: AppDirModules {
2414 ..Default::default()
2415 },
2416 global_metadata,
2417 static_siblings: Vec::new(),
2418 },
2419 },
2420 modules: AppDirModules {
2421 layout: if use_global_not_found {
2426 match modules.global_not_found {
2427 Some(v) => Some(v),
2428 None => Some(
2429 get_next_package(app_dir.clone())
2430 .await?
2431 .join("dist/client/components/builtin/global-not-found.js")?,
2432 ),
2433 }
2434 } else {
2435 modules.layout
2436 },
2437 ..not_found_root_modules
2438 },
2439 global_metadata,
2440 static_siblings: Vec::new(),
2441 }
2442 .resolved_cell();
2443
2444 {
2445 let app_page = app_page
2446 .clone_push_str("_not-found")?
2447 .complete(PageType::Page)?;
2448
2449 add_app_page(
2450 app_dir.clone(),
2451 &mut result,
2452 app_page,
2453 not_found_tree,
2454 Vec::new(),
2455 root_params,
2456 );
2457 }
2458
2459 if matches!(*next_mode.await?, NextMode::Build) {
2464 let next_package = get_next_package(app_dir.clone()).await?;
2467 let global_error_tree = AppPageLoaderTree {
2468 page: app_page.clone(),
2469 segment: directory_name.clone(),
2470 parallel_routes: fxindexmap! {
2471 rcstr!("children") => AppPageLoaderTree {
2472 page: app_page.clone(),
2473 segment: rcstr!("__PAGE__"),
2474 parallel_routes: FxIndexMap::default(),
2475 modules: AppDirModules {
2476 page: Some(next_package
2477 .join("dist/client/components/builtin/app-error.js")?),
2478 ..Default::default()
2479 },
2480 global_metadata,
2481 static_siblings: Vec::new(),
2482 }
2483 },
2484 modules: AppDirModules {
2487 global_error: modules.global_error.clone(),
2488 ..Default::default()
2489 },
2490 global_metadata,
2491 static_siblings: Vec::new(),
2492 }
2493 .resolved_cell();
2494
2495 let app_global_error_page = app_page
2496 .clone_push_str("_global-error")?
2497 .complete(PageType::Page)?;
2498 add_app_page(
2499 app_dir.clone(),
2500 &mut result,
2501 app_global_error_page,
2502 global_error_tree,
2503 Vec::new(),
2504 root_params,
2505 );
2506 }
2507 }
2508
2509 let app_page = &app_page;
2510 let directory_name = &directory_name;
2511 let subdirectories = subdirectories
2512 .iter()
2513 .map(|(subdir_name, &subdirectory)| {
2514 let app_dir = app_dir.clone();
2515
2516 async move {
2517 let mut child_app_page = app_page.clone();
2518 let mut illegal_path = None;
2519
2520 if let Err(e) = child_app_page.push_str(&normalize_underscore(subdir_name)) {
2524 illegal_path = Some(e);
2525 }
2526
2527 let map = directory_tree_to_entrypoints_internal(
2528 app_dir.clone(),
2529 *global_metadata,
2530 is_global_not_found_enabled,
2531 explicit_parallel_route_children,
2532 strict_route_matching,
2533 next_mode,
2534 subdir_name.clone(),
2535 *subdirectory,
2536 child_app_page.clone(),
2537 *root_layouts,
2538 *root_params,
2539 )
2540 .await?;
2541
2542 if let Some(illegal_path) = illegal_path
2543 && !map.is_empty()
2544 {
2545 return Err(illegal_path);
2546 }
2547
2548 let mut loader_trees = Vec::new();
2549
2550 for (_, entrypoint) in map.iter() {
2551 if let Entrypoint::AppPage { ref pages, .. } = *entrypoint {
2552 for page in pages {
2553 let app_path = AppPath::from(page.clone());
2554
2555 let loader_tree = directory_tree_to_loader_tree(
2556 app_dir.clone(),
2557 *global_metadata,
2558 directory_name.clone(),
2559 directory_tree_vc,
2560 app_page.clone(),
2561 app_path,
2562 explicit_parallel_route_children,
2563 strict_route_matching,
2564 );
2565 loader_trees.push(loader_tree);
2566 }
2567 }
2568 }
2569 Ok((map, loader_trees))
2570 }
2571 })
2572 .try_join()
2573 .await?;
2574
2575 for (map, loader_trees) in subdirectories.iter() {
2576 let mut i = 0;
2577 for (_, entrypoint) in map.iter() {
2578 match entrypoint {
2579 Entrypoint::AppPage {
2580 pages,
2581 loader_tree: _,
2582 participating_page_files: child_participating_page_files,
2583 root_params,
2584 } => {
2585 for page in pages {
2586 let loader_tree = *loader_trees[i].await?;
2587 i += 1;
2588
2589 let loader_tree = loader_tree
2590 .context("loader tree should be created for a page/default")?;
2591 let mut participating_page_files = FxIndexSet::default();
2592 loader_tree
2593 .await?
2594 .collect_page_files(&mut participating_page_files);
2595 participating_page_files
2596 .extend(child_participating_page_files.iter().cloned());
2597 add_app_page(
2598 app_dir.clone(),
2599 &mut result,
2600 page.clone(),
2601 loader_tree,
2602 participating_page_files.into_iter().collect(),
2603 *root_params,
2604 );
2605 }
2606 }
2607 Entrypoint::AppRoute {
2608 page,
2609 path,
2610 root_layouts,
2611 root_params,
2612 } => {
2613 add_app_route(
2614 app_dir.clone(),
2615 &mut result,
2616 page.clone(),
2617 path.clone(),
2618 *root_layouts,
2619 *root_params,
2620 );
2621 }
2622 Entrypoint::AppMetadata {
2623 page,
2624 metadata,
2625 root_params,
2626 } => {
2627 add_app_metadata_route(
2628 app_dir.clone(),
2629 &mut result,
2630 page.clone(),
2631 metadata.clone(),
2632 *root_params,
2633 );
2634 }
2635 }
2636 }
2637 }
2638 Ok(Vc::cell(result))
2639}
2640
2641#[turbo_tasks::function]
2643pub async fn get_global_metadata(
2644 app_dir: FileSystemPath,
2645 page_extensions: Vc<Vec<RcStr>>,
2646) -> Result<Vc<GlobalMetadata>> {
2647 let DirectoryContent::Entries(entries) = &*app_dir.read_dir().await? else {
2648 bail!("app_dir must be a directory")
2649 };
2650 let mut metadata = GlobalMetadata::default();
2651
2652 for (basename, entry) in entries {
2653 let DirectoryEntry::File(file) = entry else {
2654 continue;
2655 };
2656
2657 let Some(GlobalMetadataFileMatch {
2658 metadata_type,
2659 dynamic,
2660 }) = match_global_metadata_file(basename, &page_extensions.await?)
2661 else {
2662 continue;
2663 };
2664
2665 let entry = match metadata_type {
2666 "favicon" => &mut metadata.favicon,
2667 "manifest" => &mut metadata.manifest,
2668 "robots" => &mut metadata.robots,
2669 _ => continue,
2670 };
2671
2672 if dynamic {
2673 *entry = Some(MetadataItem::Dynamic { path: file.clone() });
2674 } else {
2675 *entry = Some(MetadataItem::Static { path: file.clone() });
2676 }
2677 }
2679
2680 Ok(metadata.cell())
2681}
2682
2683#[turbo_tasks::value(shared)]
2684struct DirectoryTreeIssue {
2685 pub severity: IssueSeverity,
2686 pub app_dir: FileSystemPath,
2687 pub message: ResolvedVc<StyledString>,
2688}
2689
2690#[async_trait]
2691#[turbo_tasks::value_impl]
2692impl Issue for DirectoryTreeIssue {
2693 fn severity(&self) -> IssueSeverity {
2694 self.severity
2695 }
2696
2697 async fn title(&self) -> Result<StyledString> {
2698 Ok(StyledString::Text(rcstr!(
2699 "An issue occurred while preparing your Next.js app"
2700 )))
2701 }
2702
2703 fn stage(&self) -> IssueStage {
2704 IssueStage::AppStructure
2705 }
2706
2707 async fn file_path(&self) -> Result<FileSystemPath> {
2708 Ok(self.app_dir.clone())
2709 }
2710
2711 async fn description(&self) -> Result<Option<StyledString>> {
2712 Ok(Some((*self.message.await?).clone()))
2713 }
2714}