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, trace::TraceRawVcs, 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, TraceRawVcs, 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, TraceRawVcs, 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, TraceRawVcs, 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 url_tree: UrlSegmentTree,
201}
202
203#[derive(Clone, Debug, Default, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
230struct UrlSegmentTree {
231 pub children: BTreeMap<RcStr, UrlSegmentTree>,
232}
233
234impl UrlSegmentTree {
235 fn static_children(&self) -> Vec<RcStr> {
236 self.children
237 .keys()
238 .filter(|name| !is_dynamic_segment(name))
239 .cloned()
240 .collect()
241 }
242
243 fn get_child(&self, segment: &str) -> Option<&UrlSegmentTree> {
244 self.children.get(segment)
245 }
246}
247
248fn build_url_segment_tree_from_subdirs(
249 subdirs: &BTreeMap<RcStr, PlainDirectoryTree>,
250) -> UrlSegmentTree {
251 let mut result = UrlSegmentTree::default();
252 build_url_segment_tree_recursive(subdirs, &mut result);
253 result
254}
255
256fn build_url_segment_tree_recursive(
263 subdirs: &BTreeMap<RcStr, PlainDirectoryTree>,
264 result: &mut UrlSegmentTree,
265) {
266 for (name, subtree) in subdirs {
267 if is_url_transparent_segment(name) {
268 build_url_segment_tree_recursive(&subtree.subdirectories, result);
272 } else {
273 let child = result.children.entry(name.clone()).or_default();
278 build_url_segment_tree_recursive(&subtree.subdirectories, child);
279 }
280 }
281}
282
283#[turbo_tasks::value_impl]
284impl DirectoryTree {
285 #[turbo_tasks::function]
286 pub async fn into_plain(&self) -> Result<Vc<PlainDirectoryTree>> {
287 let mut subdirectories = BTreeMap::new();
288
289 for (name, subdirectory) in &self.subdirectories {
290 subdirectories.insert(name.clone(), subdirectory.into_plain().owned().await?);
291 }
292
293 let url_tree = build_url_segment_tree_from_subdirs(&subdirectories);
294
295 Ok(PlainDirectoryTree {
296 subdirectories,
297 modules: self.modules.clone(),
298 url_tree,
299 }
300 .cell())
301 }
302}
303
304#[turbo_tasks::value(transparent)]
305pub struct OptionAppDir(Option<FileSystemPath>);
306
307#[turbo_tasks::function]
309pub async fn find_app_dir(project_path: FileSystemPath) -> Result<Vc<OptionAppDir>> {
310 let app = project_path.join("app")?;
311 let src_app = project_path.join("src/app")?;
312 let app_dir = if *app.get_type().await? == FileSystemEntryType::Directory {
313 app
314 } else if *src_app.get_type().await? == FileSystemEntryType::Directory {
315 src_app
316 } else {
317 return Ok(Vc::cell(None));
318 };
319
320 Ok(Vc::cell(Some(app_dir)))
321}
322
323#[turbo_tasks::function]
324async fn get_directory_tree(
325 dir: FileSystemPath,
326 page_extensions: Vc<Vec<RcStr>>,
327) -> Result<Vc<DirectoryTree>> {
328 let span = tracing::info_span!(
329 "read app directory tree",
330 name = display(dir.to_string_ref().await?)
331 );
332 get_directory_tree_internal(dir, page_extensions)
333 .instrument(span)
334 .await
335}
336
337async fn get_directory_tree_internal(
338 dir: FileSystemPath,
339 page_extensions: Vc<Vec<RcStr>>,
340) -> Result<Vc<DirectoryTree>> {
341 let DirectoryContent::Entries(entries) = &*dir.read_dir().await? else {
342 return Ok(DirectoryTree {
346 subdirectories: Default::default(),
347 modules: AppDirModules::default(),
348 }
349 .cell());
350 };
351 let page_extensions_value = page_extensions.await?;
352
353 let mut subdirectories = BTreeMap::new();
354 let mut modules = AppDirModules::default();
355
356 let mut metadata_icon = Vec::new();
357 let mut metadata_apple = Vec::new();
358 let mut metadata_open_graph = Vec::new();
359 let mut metadata_twitter = Vec::new();
360
361 for (basename, entry) in entries {
362 let entry = entry.clone().resolve_symlink().await?;
363 match entry {
364 DirectoryEntry::File(file) => {
365 if basename.ends_with(".d.ts") {
367 continue;
368 }
369 if let Some((stem, ext)) = basename.split_once('.')
370 && page_extensions_value.iter().any(|e| e == ext)
371 {
372 match stem {
373 "page" => modules.page = Some(file.clone()),
374 "layout" => modules.layout = Some(file.clone()),
375 "error" => modules.error = Some(file.clone()),
376 "global-error" => modules.global_error = Some(file.clone()),
377 "global-not-found" => modules.global_not_found = Some(file.clone()),
378 "loading" => modules.loading = Some(file.clone()),
379 "template" => modules.template = Some(file.clone()),
380 "forbidden" => modules.forbidden = Some(file.clone()),
381 "unauthorized" => modules.unauthorized = Some(file.clone()),
382 "not-found" => modules.not_found = Some(file.clone()),
383 "default" => modules.default = Some(file.clone()),
384 "route" => modules.route = Some(file.clone()),
385 _ => {}
386 }
387 }
388
389 let Some(MetadataFileMatch {
390 metadata_type,
391 number,
392 dynamic,
393 }) = match_local_metadata_file(basename.as_str(), &page_extensions_value)
394 else {
395 continue;
396 };
397
398 let entry = match metadata_type {
399 "icon" => &mut metadata_icon,
400 "apple-icon" => &mut metadata_apple,
401 "twitter-image" => &mut metadata_twitter,
402 "opengraph-image" => &mut metadata_open_graph,
403 "sitemap" => {
404 if dynamic {
405 modules.metadata.sitemap = Some(MetadataItem::Dynamic { path: file });
406 } else {
407 modules.metadata.sitemap = Some(MetadataItem::Static { path: file });
408 }
409 continue;
410 }
411 _ => continue,
412 };
413
414 if dynamic {
415 entry.push((number, MetadataWithAltItem::Dynamic { path: file }));
416 continue;
417 }
418
419 let file_name = file.file_name();
420 let basename = file_name
421 .rsplit_once('.')
422 .map_or(file_name, |(basename, _)| basename);
423 let alt_path = file.parent().join(&format!("{basename}.alt.txt"))?;
424 let alt_path = matches!(&*alt_path.get_type().await?, FileSystemEntryType::File)
425 .then_some(alt_path);
426
427 entry.push((
428 number,
429 MetadataWithAltItem::Static {
430 path: file,
431 alt_path,
432 },
433 ));
434 }
435 DirectoryEntry::Directory(dir)
436 if !basename.starts_with('_') => {
438 let result = get_directory_tree(dir.clone(), page_extensions)
439 .to_resolved()
440 .await?;
441 subdirectories.insert(basename.clone(), result);
442 }
443 _ => {}
445 }
446 }
447
448 fn sort<T>(mut list: Vec<(Option<u32>, T)>) -> Vec<T> {
449 list.sort_by_key(|(num, _)| *num);
450 list.into_iter().map(|(_, item)| item).collect()
451 }
452
453 modules.metadata.icon = sort(metadata_icon);
454 modules.metadata.apple = sort(metadata_apple);
455 modules.metadata.twitter = sort(metadata_twitter);
456 modules.metadata.open_graph = sort(metadata_open_graph);
457
458 Ok(DirectoryTree {
459 subdirectories,
460 modules,
461 }
462 .cell())
463}
464
465#[turbo_tasks::value]
466#[derive(Debug, Clone)]
467pub struct AppPageLoaderTree {
468 pub page: AppPage,
469 pub segment: RcStr,
470 #[bincode(with = "turbo_bincode::indexmap")]
471 pub parallel_routes: FxIndexMap<RcStr, AppPageLoaderTree>,
472 pub modules: AppDirModules,
473 pub global_metadata: ResolvedVc<GlobalMetadata>,
474 pub static_siblings: Vec<RcStr>,
478}
479
480impl AppPageLoaderTree {
481 pub fn has_page(&self) -> bool {
483 if &*self.segment == "__PAGE__" {
484 return true;
485 }
486
487 for (_, tree) in &self.parallel_routes {
488 if tree.has_page() {
489 return true;
490 }
491 }
492
493 false
494 }
495
496 pub fn has_only_catchall(&self) -> bool {
499 if &*self.segment == "__PAGE__" && !self.page.is_catchall() {
500 return false;
501 }
502
503 for (_, tree) in &self.parallel_routes {
504 if !tree.has_only_catchall() {
505 return false;
506 }
507 }
508
509 true
510 }
511
512 pub fn is_intercepting(&self) -> bool {
514 if self.page.is_intercepting() && self.has_page() {
515 return true;
516 }
517
518 for (_, tree) in &self.parallel_routes {
519 if tree.is_intercepting() {
520 return true;
521 }
522 }
523
524 false
525 }
526
527 pub fn get_specificity(&self) -> usize {
530 if &*self.segment == "__PAGE__" {
531 return AppPath::from(self.page.clone()).len();
532 }
533
534 let mut specificity = 0;
535
536 for (_, tree) in &self.parallel_routes {
537 specificity = specificity.max(tree.get_specificity());
538 }
539
540 specificity
541 }
542}
543
544#[turbo_tasks::value(transparent)]
545#[derive(Default)]
546pub struct RootParamVecOption(Option<Vec<RcStr>>);
547
548#[turbo_tasks::value_impl]
549impl ValueDefault for RootParamVecOption {
550 #[turbo_tasks::function]
551 fn value_default() -> Vc<Self> {
552 Vc::cell(Default::default())
553 }
554}
555
556#[turbo_tasks::value(transparent)]
557pub struct FileSystemPathVec(Vec<FileSystemPath>);
558
559#[turbo_tasks::value_impl]
560impl ValueDefault for FileSystemPathVec {
561 #[turbo_tasks::function]
562 fn value_default() -> Vc<Self> {
563 Vc::cell(Vec::new())
564 }
565}
566
567#[turbo_tasks::task_input]
568#[derive(Clone, PartialEq, Eq, Hash, TraceRawVcs, ValueDebugFormat, Debug, Encode, Decode)]
569pub enum Entrypoint {
570 AppPage {
571 pages: Vec<AppPage>,
572 loader_tree: ResolvedVc<AppPageLoaderTree>,
573 root_params: ResolvedVc<RootParamVecOption>,
574 },
575 AppRoute {
576 page: AppPage,
577 path: FileSystemPath,
578 root_layouts: ResolvedVc<FileSystemPathVec>,
579 root_params: ResolvedVc<RootParamVecOption>,
580 },
581 AppMetadata {
582 page: AppPage,
583 metadata: MetadataItem,
584 root_params: ResolvedVc<RootParamVecOption>,
585 },
586}
587
588impl Entrypoint {
589 pub fn page(&self) -> &AppPage {
590 match self {
591 Entrypoint::AppPage { pages, .. } => pages.first().unwrap(),
592 Entrypoint::AppRoute { page, .. } => page,
593 Entrypoint::AppMetadata { page, .. } => page,
594 }
595 }
596 pub fn root_params(&self) -> ResolvedVc<RootParamVecOption> {
597 match self {
598 Entrypoint::AppPage { root_params, .. } => *root_params,
599 Entrypoint::AppRoute { root_params, .. } => *root_params,
600 Entrypoint::AppMetadata { root_params, .. } => *root_params,
601 }
602 }
603}
604
605#[turbo_tasks::value(transparent)]
606pub struct Entrypoints(
607 #[bincode(with = "turbo_bincode::indexmap")] FxIndexMap<AppPath, Entrypoint>,
608);
609
610fn is_parallel_route(name: &str) -> bool {
611 name.starts_with('@')
612}
613
614fn is_group_route(name: &str) -> bool {
615 name.starts_with('(') && name.ends_with(')')
616}
617
618fn is_url_transparent_segment(name: &str) -> bool {
622 is_group_route(name) || is_parallel_route(name)
623}
624
625fn is_dynamic_segment(name: &str) -> bool {
626 name.starts_with('[') && name.ends_with(']')
627}
628
629fn match_parallel_route(name: &str) -> Option<&str> {
630 name.strip_prefix('@')
631}
632
633fn conflict_issue(
634 app_dir: FileSystemPath,
635 e: &'_ OccupiedEntry<'_, AppPath, Entrypoint>,
636 a: &str,
637 b: &str,
638 value_a: &AppPage,
639 value_b: &AppPage,
640) {
641 let item_names = if a == b {
642 format!("{a}s")
643 } else {
644 format!("{a} and {b}")
645 };
646
647 DirectoryTreeIssue {
648 app_dir,
649 message: StyledString::Text(
650 format!(
651 "Conflicting {} at {}: {a} at {value_a} and {b} at {value_b}",
652 item_names,
653 e.key(),
654 )
655 .into(),
656 )
657 .resolved_cell(),
658 severity: IssueSeverity::Error,
659 }
660 .resolved_cell()
661 .emit();
662}
663
664fn add_app_page(
665 app_dir: FileSystemPath,
666 result: &mut FxIndexMap<AppPath, Entrypoint>,
667 page: AppPage,
668 loader_tree: ResolvedVc<AppPageLoaderTree>,
669 root_params: ResolvedVc<RootParamVecOption>,
670) {
671 let mut e = match result.entry(page.clone().into()) {
672 Entry::Occupied(e) => e,
673 Entry::Vacant(e) => {
674 e.insert(Entrypoint::AppPage {
675 pages: vec![page],
676 loader_tree,
677 root_params,
678 });
679 return;
680 }
681 };
682
683 let conflict = |existing_name: &str, existing_page: &AppPage| {
684 conflict_issue(app_dir, &e, "page", existing_name, &page, existing_page);
685 };
686
687 let value = e.get();
688 match value {
689 Entrypoint::AppPage {
690 pages: existing_pages,
691 loader_tree: existing_loader_tree,
692 ..
693 } => {
694 if *existing_loader_tree != loader_tree {
697 conflict("page", existing_pages.first().unwrap());
698 }
699
700 let Entrypoint::AppPage {
701 pages: stored_pages,
702 ..
703 } = e.get_mut()
704 else {
705 unreachable!("Entrypoint::AppPage was already matched");
706 };
707
708 stored_pages.push(page);
709 stored_pages.sort();
710 }
711 Entrypoint::AppRoute {
712 page: existing_page,
713 ..
714 } => {
715 conflict("route", existing_page);
716 }
717 Entrypoint::AppMetadata {
718 page: existing_page,
719 ..
720 } => {
721 conflict("metadata", existing_page);
722 }
723 }
724}
725
726fn add_app_route(
727 app_dir: FileSystemPath,
728 result: &mut FxIndexMap<AppPath, Entrypoint>,
729 page: AppPage,
730 path: FileSystemPath,
731 root_layouts: ResolvedVc<FileSystemPathVec>,
732 root_params: ResolvedVc<RootParamVecOption>,
733) {
734 let e = match result.entry(page.clone().into()) {
735 Entry::Occupied(e) => e,
736 Entry::Vacant(e) => {
737 e.insert(Entrypoint::AppRoute {
738 page,
739 path,
740 root_layouts,
741 root_params,
742 });
743 return;
744 }
745 };
746
747 let conflict = |existing_name: &str, existing_page: &AppPage| {
748 conflict_issue(app_dir, &e, "route", existing_name, &page, existing_page);
749 };
750
751 let value = e.get();
752 match value {
753 Entrypoint::AppPage { pages, .. } => {
754 conflict("page", pages.first().unwrap());
755 }
756 Entrypoint::AppRoute {
757 page: existing_page,
758 ..
759 } => {
760 conflict("route", existing_page);
761 }
762 Entrypoint::AppMetadata {
763 page: existing_page,
764 ..
765 } => {
766 conflict("metadata", existing_page);
767 }
768 }
769}
770
771fn add_app_metadata_route(
772 app_dir: FileSystemPath,
773 result: &mut FxIndexMap<AppPath, Entrypoint>,
774 page: AppPage,
775 metadata: MetadataItem,
776 root_params: ResolvedVc<RootParamVecOption>,
777) {
778 let e = match result.entry(page.clone().into()) {
779 Entry::Occupied(e) => e,
780 Entry::Vacant(e) => {
781 e.insert(Entrypoint::AppMetadata {
782 page,
783 metadata,
784 root_params,
785 });
786 return;
787 }
788 };
789
790 let conflict = |existing_name: &str, existing_page: &AppPage| {
791 conflict_issue(app_dir, &e, "metadata", existing_name, &page, existing_page);
792 };
793
794 let value = e.get();
795 match value {
796 Entrypoint::AppPage { pages, .. } => {
797 conflict("page", pages.first().unwrap());
798 }
799 Entrypoint::AppRoute {
800 page: existing_page,
801 ..
802 } => {
803 conflict("route", existing_page);
804 }
805 Entrypoint::AppMetadata {
806 page: existing_page,
807 ..
808 } => {
809 conflict("metadata", existing_page);
810 }
811 }
812}
813
814#[turbo_tasks::function]
815pub fn get_entrypoints(
816 app_dir: FileSystemPath,
817 page_extensions: Vc<Vec<RcStr>>,
818 is_global_not_found_enabled: Vc<bool>,
819 next_mode: Vc<NextMode>,
820) -> Vc<Entrypoints> {
821 directory_tree_to_entrypoints(
822 app_dir.clone(),
823 get_directory_tree(app_dir.clone(), page_extensions),
824 get_global_metadata(app_dir, page_extensions),
825 is_global_not_found_enabled,
826 next_mode,
827 Default::default(),
828 Default::default(),
829 )
830}
831
832#[turbo_tasks::value(transparent)]
833pub struct CollectedRootParams(#[bincode(with = "turbo_bincode::indexset")] FxIndexSet<RcStr>);
834
835#[turbo_tasks::function]
836pub async fn collect_root_params(
837 entrypoints: ResolvedVc<Entrypoints>,
838) -> Result<Vc<CollectedRootParams>> {
839 let mut collected_root_params = FxIndexSet::<RcStr>::default();
840 for (_, entrypoint) in entrypoints.await?.iter() {
841 if let Some(ref root_params) = *entrypoint.root_params().await? {
842 collected_root_params.extend(root_params.iter().cloned());
843 }
844 }
845 Ok(Vc::cell(collected_root_params))
846}
847
848#[turbo_tasks::function]
849fn directory_tree_to_entrypoints(
850 app_dir: FileSystemPath,
851 directory_tree: Vc<DirectoryTree>,
852 global_metadata: Vc<GlobalMetadata>,
853 is_global_not_found_enabled: Vc<bool>,
854 next_mode: Vc<NextMode>,
855 root_layouts: Vc<FileSystemPathVec>,
856 root_params: Vc<RootParamVecOption>,
857) -> Vc<Entrypoints> {
858 directory_tree_to_entrypoints_internal(
859 app_dir,
860 global_metadata,
861 is_global_not_found_enabled,
862 next_mode,
863 rcstr!(""),
864 directory_tree,
865 AppPage::new(),
866 root_layouts,
867 root_params,
868 )
869}
870
871#[turbo_tasks::value]
872struct DuplicateParallelRouteIssue {
873 app_dir: FileSystemPath,
874 previously_inserted_page: AppPage,
875 page: AppPage,
876}
877
878#[async_trait]
879#[turbo_tasks::value_impl]
880impl Issue for DuplicateParallelRouteIssue {
881 async fn file_path(&self) -> Result<FileSystemPath> {
882 self.app_dir.join(&self.page.to_string())
883 }
884
885 fn stage(&self) -> IssueStage {
886 IssueStage::ProcessModule
887 }
888
889 async fn title(&self) -> Result<StyledString> {
890 Ok(StyledString::Text(
891 format!(
892 "You cannot have two parallel pages that resolve to the same path. Please check \
893 {} and {}.",
894 self.previously_inserted_page, self.page
895 )
896 .into(),
897 ))
898 }
899}
900
901#[turbo_tasks::value]
902struct MissingDefaultParallelRouteIssue {
903 app_dir: FileSystemPath,
904 app_page: AppPage,
905 slot_name: RcStr,
906}
907
908#[turbo_tasks::function]
909fn missing_default_parallel_route_issue(
910 app_dir: FileSystemPath,
911 app_page: AppPage,
912 slot_name: RcStr,
913) -> Vc<MissingDefaultParallelRouteIssue> {
914 MissingDefaultParallelRouteIssue {
915 app_dir,
916 app_page,
917 slot_name,
918 }
919 .cell()
920}
921
922#[async_trait]
923#[turbo_tasks::value_impl]
924impl Issue for MissingDefaultParallelRouteIssue {
925 async fn file_path(&self) -> Result<FileSystemPath> {
926 self.app_dir
927 .join(&self.app_page.to_string())?
928 .join(&format!("@{}", self.slot_name))
929 }
930
931 fn stage(&self) -> IssueStage {
932 IssueStage::AppStructure
933 }
934
935 fn severity(&self) -> IssueSeverity {
936 IssueSeverity::Error
937 }
938
939 async fn title(&self) -> Result<StyledString> {
940 Ok(StyledString::Text(
941 format!(
942 "Missing required default.js file for parallel route at {}/@{}",
943 self.app_page, self.slot_name
944 )
945 .into(),
946 ))
947 }
948
949 async fn description(&self) -> Result<Option<StyledString>> {
950 Ok(Some(StyledString::Stack(vec![
951 StyledString::Text(
952 format!(
953 "The parallel route slot \"@{}\" is missing a default.js file. When using \
954 parallel routes, each slot must have a default.js file to serve as a \
955 fallback.",
956 self.slot_name
957 )
958 .into(),
959 ),
960 StyledString::Text(
961 format!(
962 "Create a default.js file at: {}/@{}/default.js",
963 self.app_page, self.slot_name
964 )
965 .into(),
966 ),
967 ])))
968 }
969
970 fn documentation_link(&self) -> RcStr {
971 rcstr!("https://nextjs.org/docs/messages/slot-missing-default")
972 }
973}
974
975fn page_path_except_parallel(loader_tree: &AppPageLoaderTree) -> Option<AppPage> {
976 if loader_tree.page.iter().any(|v| {
977 matches!(
978 v,
979 PageSegment::CatchAll(..)
980 | PageSegment::OptionalCatchAll(..)
981 | PageSegment::Parallel(..)
982 )
983 }) {
984 return None;
985 }
986
987 if loader_tree.modules.page.is_some() {
988 return Some(loader_tree.page.clone());
989 }
990
991 if let Some(children) = loader_tree.parallel_routes.get("children") {
992 return page_path_except_parallel(children);
993 }
994
995 None
996}
997
998fn has_child_routes(directory_tree: &PlainDirectoryTree) -> bool {
1002 for (name, subdirectory) in &directory_tree.subdirectories {
1003 if is_parallel_route(name) {
1005 continue;
1006 }
1007
1008 if is_group_route(name) {
1010 if has_child_routes(subdirectory) {
1012 return true;
1013 }
1014 continue;
1015 }
1016
1017 return true;
1019 }
1020
1021 false
1022}
1023
1024async fn check_duplicate(
1025 duplicate: &mut FxHashMap<AppPath, AppPage>,
1026 loader_tree: &AppPageLoaderTree,
1027 app_dir: FileSystemPath,
1028) -> Result<()> {
1029 let page_path = page_path_except_parallel(loader_tree);
1030
1031 if let Some(page_path) = page_path
1032 && let Some(prev) = duplicate.insert(AppPath::from(page_path.clone()), page_path.clone())
1033 && prev != page_path
1034 {
1035 DuplicateParallelRouteIssue {
1036 app_dir: app_dir.clone(),
1037 previously_inserted_page: prev.clone(),
1038 page: loader_tree.page.clone(),
1039 }
1040 .resolved_cell()
1041 .emit();
1042 }
1043
1044 Ok(())
1045}
1046
1047#[turbo_tasks::value(transparent)]
1048struct AppPageLoaderTreeOption(Option<ResolvedVc<AppPageLoaderTree>>);
1049
1050#[turbo_tasks::function]
1052async fn directory_tree_to_loader_tree(
1053 app_dir: FileSystemPath,
1054 global_metadata: Vc<GlobalMetadata>,
1055 directory_name: RcStr,
1056 directory_tree: Vc<DirectoryTree>,
1057 app_page: AppPage,
1058 for_app_path: AppPath,
1060) -> Result<Vc<AppPageLoaderTreeOption>> {
1061 let plain_tree_vc = directory_tree.into_plain();
1062 let plain_tree = &*plain_tree_vc.await?;
1063
1064 let tree = directory_tree_to_loader_tree_internal(
1065 app_dir,
1066 global_metadata,
1067 directory_name,
1068 plain_tree,
1069 app_page,
1070 for_app_path,
1071 AppDirModules::default(),
1072 Some(&plain_tree.url_tree),
1073 )
1074 .await?;
1075
1076 Ok(Vc::cell(tree.map(AppPageLoaderTree::resolved_cell)))
1077}
1078
1079async fn check_and_update_module_references(
1094 app_dir: FileSystemPath,
1095 module: &mut Option<FileSystemPath>,
1096 parent_module: &mut Option<FileSystemPath>,
1097 file_path: &str,
1098 is_first_layer_group_route: bool,
1099) -> Result<()> {
1100 match (module.as_mut(), parent_module.as_mut()) {
1101 (Some(module), _) => *parent_module = Some(module.clone()),
1103 (None, Some(parent_module)) if is_first_layer_group_route => {
1106 *module = Some(parent_module.clone())
1107 }
1108 (None, Some(_)) => {}
1111 (None, None) => {
1115 let default_page = get_next_package(app_dir).await?.join(file_path)?;
1116 *module = Some(default_page.clone());
1117 *parent_module = Some(default_page);
1118 }
1119 }
1120
1121 Ok(())
1122}
1123
1124async fn check_and_update_global_module_references(
1132 app_dir: FileSystemPath,
1133 module: &mut Option<FileSystemPath>,
1134 file_path: &str,
1135) -> Result<()> {
1136 if module.is_none() {
1137 *module = Some(get_next_package(app_dir).await?.join(file_path)?);
1138 }
1139
1140 Ok(())
1141}
1142
1143async fn directory_tree_to_loader_tree_internal(
1144 app_dir: FileSystemPath,
1145 global_metadata: Vc<GlobalMetadata>,
1146 directory_name: RcStr,
1147 directory_tree: &PlainDirectoryTree,
1148 app_page: AppPage,
1149 for_app_path: AppPath,
1151 mut parent_modules: AppDirModules,
1152 url_tree: Option<&UrlSegmentTree>,
1153) -> Result<Option<AppPageLoaderTree>> {
1154 let app_path = AppPath::from(app_page.clone());
1155
1156 if !for_app_path.contains(&app_path) {
1157 return Ok(None);
1158 }
1159
1160 let mut modules = directory_tree.modules.clone();
1161
1162 modules.metadata.base_page = Some(app_page.clone());
1165
1166 let is_root_directory = app_page.is_root();
1168
1169 let is_first_layer_group_route = app_page.is_first_layer_group_route();
1171
1172 if is_root_directory || is_first_layer_group_route {
1175 check_and_update_module_references(
1176 app_dir.clone(),
1177 &mut modules.not_found,
1178 &mut parent_modules.not_found,
1179 "dist/client/components/builtin/not-found.js",
1180 is_first_layer_group_route,
1181 )
1182 .await?;
1183
1184 check_and_update_module_references(
1185 app_dir.clone(),
1186 &mut modules.forbidden,
1187 &mut parent_modules.forbidden,
1188 "dist/client/components/builtin/forbidden.js",
1189 is_first_layer_group_route,
1190 )
1191 .await?;
1192
1193 check_and_update_module_references(
1194 app_dir.clone(),
1195 &mut modules.unauthorized,
1196 &mut parent_modules.unauthorized,
1197 "dist/client/components/builtin/unauthorized.js",
1198 is_first_layer_group_route,
1199 )
1200 .await?;
1201 }
1202
1203 if is_root_directory {
1204 check_and_update_global_module_references(
1205 app_dir.clone(),
1206 &mut modules.global_error,
1207 "dist/client/components/builtin/global-error.js",
1208 )
1209 .await?;
1210 }
1211
1212 let static_siblings: Vec<RcStr> = if is_dynamic_segment(&directory_name) {
1216 url_tree
1217 .map(|t| {
1218 t.static_children()
1219 .into_iter()
1220 .filter(|s| s != &directory_name)
1221 .collect()
1222 })
1223 .unwrap_or_default()
1224 } else {
1225 Vec::new()
1227 };
1228
1229 let mut tree = AppPageLoaderTree {
1230 page: app_page.clone(),
1231 segment: directory_name.clone(),
1232 parallel_routes: FxIndexMap::default(),
1233 modules: modules.without_leaves(),
1234 global_metadata: global_metadata.to_resolved().await?,
1235 static_siblings,
1236 };
1237
1238 let current_level_is_parallel_route = is_parallel_route(&directory_name);
1239
1240 if current_level_is_parallel_route {
1241 tree.segment = rcstr!("(__SLOT__)");
1242 }
1243
1244 if let Some(page) = (app_path == for_app_path || app_path.is_catchall())
1245 .then_some(modules.page)
1246 .flatten()
1247 {
1248 tree.parallel_routes.insert(
1249 rcstr!("children"),
1250 AppPageLoaderTree {
1251 page: app_page.clone(),
1252 segment: rcstr!("__PAGE__"),
1253 parallel_routes: FxIndexMap::default(),
1254 modules: AppDirModules {
1255 page: Some(page),
1256 metadata: modules.metadata,
1257 ..Default::default()
1258 },
1259 global_metadata: global_metadata.to_resolved().await?,
1260 static_siblings: Vec::new(),
1261 },
1262 );
1263 }
1264
1265 let mut duplicate = FxHashMap::default();
1266
1267 for (subdir_name, subdirectory) in &directory_tree.subdirectories {
1268 let parallel_route_key = match_parallel_route(subdir_name);
1269
1270 let mut child_app_page = app_page.clone();
1271 let mut illegal_path_error = None;
1272
1273 if let Err(e) = child_app_page.push_str(&normalize_underscore(subdir_name)) {
1277 illegal_path_error = Some(e);
1278 }
1279
1280 let child_url_tree: Option<&UrlSegmentTree> =
1282 if directory_name.is_empty() || is_url_transparent_segment(&directory_name) {
1283 url_tree
1284 } else {
1285 url_tree.and_then(|t| t.get_child(&directory_name))
1286 };
1287
1288 let subtree = Box::pin(directory_tree_to_loader_tree_internal(
1289 app_dir.clone(),
1290 global_metadata,
1291 subdir_name.clone(),
1292 subdirectory,
1293 child_app_page.clone(),
1294 for_app_path.clone(),
1295 parent_modules.clone(),
1296 child_url_tree,
1297 ))
1298 .await?;
1299
1300 if let Some(illegal_path) = subtree.as_ref().and(illegal_path_error) {
1301 return Err(illegal_path);
1302 }
1303
1304 if let Some(subtree) = subtree {
1305 if let Some(key) = parallel_route_key {
1306 let is_inside_catchall = app_page.is_catchall();
1317
1318 let is_leaf_segment = !has_child_routes(directory_tree);
1326
1327 let slot_has_children = has_child_routes(subdirectory);
1338
1339 if key != "children"
1340 && subdirectory.modules.default.is_none()
1341 && !is_inside_catchall
1342 && !is_leaf_segment
1343 && !slot_has_children
1344 {
1345 missing_default_parallel_route_issue(
1346 app_dir.clone(),
1347 app_page.clone(),
1348 key.into(),
1349 )
1350 .to_resolved()
1351 .await?
1352 .emit();
1353 }
1354
1355 tree.parallel_routes.insert(key.into(), subtree);
1356 continue;
1357 }
1358
1359 if is_group_route(subdir_name) && !subtree.has_page() {
1361 continue;
1362 }
1363
1364 if subtree.has_page() {
1365 check_duplicate(&mut duplicate, &subtree, app_dir.clone()).await?;
1366 }
1367
1368 if let Some(current_tree) = tree.parallel_routes.get("children") {
1369 if current_tree.has_only_catchall()
1370 && (!subtree.has_only_catchall()
1371 || current_tree.get_specificity() < subtree.get_specificity())
1372 {
1373 tree.parallel_routes
1374 .insert(rcstr!("children"), subtree.clone());
1375 }
1376 } else {
1377 tree.parallel_routes.insert(rcstr!("children"), subtree);
1378 }
1379 } else if let Some(key) = parallel_route_key {
1380 bail!(
1381 "missing page or default for parallel route `{}` (page: {})",
1382 key,
1383 app_page
1384 );
1385 }
1386 }
1387
1388 if tree
1391 .parallel_routes
1392 .iter()
1393 .any(|(_, parallel_tree)| parallel_tree.is_intercepting())
1394 {
1395 let mut keys_to_replace = Vec::new();
1396
1397 for (key, parallel_tree) in &tree.parallel_routes {
1398 if !parallel_tree.is_intercepting() {
1399 keys_to_replace.push(key.clone());
1400 }
1401 }
1402
1403 for key in keys_to_replace {
1404 let subdir_name: RcStr = format!("@{key}").into();
1405
1406 let default = if key == "children" {
1407 modules.default.clone()
1408 } else if let Some(subdirectory) = directory_tree.subdirectories.get(&subdir_name) {
1409 subdirectory.modules.default.clone()
1410 } else {
1411 None
1412 };
1413
1414 let is_inside_catchall = app_page.is_catchall();
1415
1416 let is_leaf_segment = !has_child_routes(directory_tree);
1418
1419 if default.is_none() && key != "children" && !is_inside_catchall && !is_leaf_segment {
1424 missing_default_parallel_route_issue(
1425 app_dir.clone(),
1426 app_page.clone(),
1427 key.clone(),
1428 )
1429 .to_resolved()
1430 .await?
1431 .emit();
1432 }
1433
1434 tree.parallel_routes.insert(
1435 key.clone(),
1436 default_route_tree(
1437 app_dir.clone(),
1438 global_metadata,
1439 app_page.clone(),
1440 default,
1441 key.clone(),
1442 for_app_path.clone(),
1443 )
1444 .await?,
1445 );
1446 }
1447 }
1448
1449 if tree.parallel_routes.is_empty() {
1450 if modules.default.is_some() || current_level_is_parallel_route {
1451 tree = default_route_tree(
1452 app_dir.clone(),
1453 global_metadata,
1454 app_page.clone(),
1455 modules.default.clone(),
1456 rcstr!("children"),
1457 for_app_path.clone(),
1458 )
1459 .await?;
1460 } else {
1461 return Ok(None);
1462 }
1463 } else if tree.parallel_routes.get("children").is_none() {
1464 tree.parallel_routes.insert(
1465 rcstr!("children"),
1466 default_route_tree(
1467 app_dir.clone(),
1468 global_metadata,
1469 app_page.clone(),
1470 modules.default.clone(),
1471 rcstr!("children"),
1472 for_app_path.clone(),
1473 )
1474 .await?,
1475 );
1476 }
1477
1478 if tree.parallel_routes.len() > 1
1479 && tree.parallel_routes.keys().next().map(|s| s.as_str()) != Some("children")
1480 {
1481 tree.parallel_routes
1483 .move_index(tree.parallel_routes.len() - 1, 0);
1484 }
1485
1486 Ok(Some(tree))
1487}
1488
1489async fn default_route_tree(
1490 app_dir: FileSystemPath,
1491 global_metadata: Vc<GlobalMetadata>,
1492 app_page: AppPage,
1493 default_component: Option<FileSystemPath>,
1494 slot_name: RcStr,
1495 for_app_path: AppPath,
1496) -> Result<AppPageLoaderTree> {
1497 Ok(AppPageLoaderTree {
1498 page: app_page.clone(),
1499 segment: rcstr!("__DEFAULT__"),
1500 parallel_routes: FxIndexMap::default(),
1501 modules: if let Some(default) = default_component {
1502 AppDirModules {
1503 default: Some(default),
1504 ..Default::default()
1505 }
1506 } else {
1507 let contains_interception = for_app_path.contains_interception();
1508
1509 let default_file = if contains_interception && slot_name == "children" {
1510 "dist/client/components/builtin/default-null.js"
1511 } else {
1512 "dist/client/components/builtin/default.js"
1513 };
1514
1515 AppDirModules {
1516 default: Some(get_next_package(app_dir).await?.join(default_file)?),
1517 ..Default::default()
1518 }
1519 },
1520 global_metadata: global_metadata.to_resolved().await?,
1521 static_siblings: Vec::new(),
1522 })
1523}
1524
1525#[turbo_tasks::function]
1526async fn directory_tree_to_entrypoints_internal(
1527 app_dir: FileSystemPath,
1528 global_metadata: ResolvedVc<GlobalMetadata>,
1529 is_global_not_found_enabled: Vc<bool>,
1530 next_mode: Vc<NextMode>,
1531 directory_name: RcStr,
1532 directory_tree: Vc<DirectoryTree>,
1533 app_page: AppPage,
1534 root_layouts: ResolvedVc<FileSystemPathVec>,
1535 root_params: ResolvedVc<RootParamVecOption>,
1536) -> Result<Vc<Entrypoints>> {
1537 let span = tracing::info_span!("build layout trees", name = display(&app_page));
1538 directory_tree_to_entrypoints_internal_untraced(
1539 app_dir,
1540 global_metadata,
1541 is_global_not_found_enabled,
1542 next_mode,
1543 directory_name,
1544 directory_tree,
1545 app_page,
1546 root_layouts,
1547 root_params,
1548 )
1549 .instrument(span)
1550 .await
1551}
1552
1553async fn directory_tree_to_entrypoints_internal_untraced(
1554 app_dir: FileSystemPath,
1555 global_metadata: ResolvedVc<GlobalMetadata>,
1556 is_global_not_found_enabled: Vc<bool>,
1557 next_mode: Vc<NextMode>,
1558 directory_name: RcStr,
1559 directory_tree: Vc<DirectoryTree>,
1560 app_page: AppPage,
1561 root_layouts: ResolvedVc<FileSystemPathVec>,
1562 root_params: ResolvedVc<RootParamVecOption>,
1563) -> Result<Vc<Entrypoints>> {
1564 let mut result = FxIndexMap::default();
1565
1566 let directory_tree_vc = directory_tree;
1567 let directory_tree = &*directory_tree.await?;
1568
1569 let subdirectories = &directory_tree.subdirectories;
1570 let modules = &directory_tree.modules;
1571 let root_layouts = if let Some(layout) = &modules.layout {
1575 let mut layouts = root_layouts.owned().await?;
1576 layouts.push(layout.clone());
1577 ResolvedVc::cell(layouts)
1578 } else {
1579 root_layouts
1580 };
1581
1582 let root_params = if root_params.await?.is_none() && (*root_layouts.await?).len() == 1 {
1584 ResolvedVc::cell(Some(
1587 app_page
1588 .0
1589 .iter()
1590 .filter_map(|segment| match segment {
1591 PageSegment::Dynamic(param)
1592 | PageSegment::CatchAll(param)
1593 | PageSegment::OptionalCatchAll(param) => Some(param.clone()),
1594 _ => None,
1595 })
1596 .collect::<Vec<RcStr>>(),
1597 ))
1598 } else {
1599 root_params
1600 };
1601
1602 if modules.page.is_some() {
1603 let app_path = AppPath::from(app_page.clone());
1604
1605 let loader_tree = *directory_tree_to_loader_tree(
1606 app_dir.clone(),
1607 *global_metadata,
1608 directory_name.clone(),
1609 directory_tree_vc,
1610 app_page.clone(),
1611 app_path,
1612 )
1613 .await?;
1614
1615 add_app_page(
1616 app_dir.clone(),
1617 &mut result,
1618 app_page.complete(PageType::Page)?,
1619 loader_tree.context("loader tree should be created for a page/default")?,
1620 root_params,
1621 );
1622 }
1623
1624 if let Some(route) = &modules.route {
1625 add_app_route(
1626 app_dir.clone(),
1627 &mut result,
1628 app_page.complete(PageType::Route)?,
1629 route.clone(),
1630 root_layouts,
1631 root_params,
1632 );
1633 }
1634
1635 let Metadata {
1636 icon,
1637 apple,
1638 twitter,
1639 open_graph,
1640 sitemap,
1641 base_page: _,
1642 } = &modules.metadata;
1643
1644 for meta in sitemap
1645 .iter()
1646 .cloned()
1647 .chain(icon.iter().cloned().map(MetadataItem::from))
1648 .chain(apple.iter().cloned().map(MetadataItem::from))
1649 .chain(twitter.iter().cloned().map(MetadataItem::from))
1650 .chain(open_graph.iter().cloned().map(MetadataItem::from))
1651 {
1652 let app_page = app_page.clone_push_str(&get_metadata_route_name(meta.clone()).await?)?;
1653
1654 add_app_metadata_route(
1655 app_dir.clone(),
1656 &mut result,
1657 normalize_metadata_route(app_page)?,
1658 meta,
1659 root_params,
1660 );
1661 }
1662
1663 if app_page.is_root() {
1665 let GlobalMetadata {
1666 favicon,
1667 robots,
1668 manifest,
1669 } = &*global_metadata.await?;
1670
1671 for meta in favicon.iter().chain(robots.iter()).chain(manifest.iter()) {
1672 let app_page =
1673 app_page.clone_push_str(&get_metadata_route_name(meta.clone()).await?)?;
1674
1675 add_app_metadata_route(
1676 app_dir.clone(),
1677 &mut result,
1678 normalize_metadata_route(app_page)?,
1679 meta.clone(),
1680 root_params,
1681 );
1682 }
1683
1684 let mut modules = directory_tree.modules.clone();
1685
1686 if modules.layout.is_none() {
1688 modules.layout = Some(
1689 get_next_package(app_dir.clone())
1690 .await?
1691 .join("dist/client/components/builtin/layout.js")?,
1692 );
1693 }
1694
1695 if modules.not_found.is_none() {
1696 modules.not_found = Some(
1697 get_next_package(app_dir.clone())
1698 .await?
1699 .join("dist/client/components/builtin/not-found.js")?,
1700 );
1701 }
1702 if modules.forbidden.is_none() {
1703 modules.forbidden = Some(
1704 get_next_package(app_dir.clone())
1705 .await?
1706 .join("dist/client/components/builtin/forbidden.js")?,
1707 );
1708 }
1709 if modules.unauthorized.is_none() {
1710 modules.unauthorized = Some(
1711 get_next_package(app_dir.clone())
1712 .await?
1713 .join("dist/client/components/builtin/unauthorized.js")?,
1714 );
1715 }
1716 if modules.global_error.is_none() {
1717 modules.global_error = Some(
1718 get_next_package(app_dir.clone())
1719 .await?
1720 .join("dist/client/components/builtin/global-error.js")?,
1721 );
1722 }
1723
1724 let is_global_not_found_enabled = *is_global_not_found_enabled.await?;
1729 let use_global_not_found =
1730 is_global_not_found_enabled || modules.global_not_found.is_some();
1731
1732 let not_found_root_modules = modules.without_leaves();
1733 let not_found_tree = AppPageLoaderTree {
1734 page: app_page.clone(),
1735 segment: directory_name.clone(),
1736 parallel_routes: fxindexmap! {
1737 rcstr!("children") => AppPageLoaderTree {
1738 page: app_page.clone(),
1739 segment: rcstr!("/_not-found"),
1740 parallel_routes: fxindexmap! {
1741 rcstr!("children") => AppPageLoaderTree {
1742 page: app_page.clone(),
1743 segment: rcstr!("__PAGE__"),
1744 parallel_routes: FxIndexMap::default(),
1745 modules: if use_global_not_found {
1746 AppDirModules {
1749 page: Some(get_next_package(app_dir.clone())
1751 .await?
1752 .join("dist/client/components/builtin/empty-stub.js")?,
1753 ),
1754 ..Default::default()
1755 }
1756 } else {
1757 AppDirModules {
1760 page: match modules.not_found {
1761 Some(v) => Some(v),
1762 None => Some(get_next_package(app_dir.clone())
1763 .await?
1764 .join("dist/client/components/builtin/not-found.js")?,
1765 ),
1766 },
1767 ..Default::default()
1768 }
1769 },
1770 global_metadata,
1771 static_siblings: Vec::new(),
1772 }
1773 },
1774 modules: AppDirModules {
1775 ..Default::default()
1776 },
1777 global_metadata,
1778 static_siblings: Vec::new(),
1779 },
1780 },
1781 modules: AppDirModules {
1782 layout: if use_global_not_found {
1787 match modules.global_not_found {
1788 Some(v) => Some(v),
1789 None => Some(
1790 get_next_package(app_dir.clone())
1791 .await?
1792 .join("dist/client/components/builtin/global-not-found.js")?,
1793 ),
1794 }
1795 } else {
1796 modules.layout
1797 },
1798 ..not_found_root_modules
1799 },
1800 global_metadata,
1801 static_siblings: Vec::new(),
1802 }
1803 .resolved_cell();
1804
1805 {
1806 let app_page = app_page
1807 .clone_push_str("_not-found")?
1808 .complete(PageType::Page)?;
1809
1810 add_app_page(
1811 app_dir.clone(),
1812 &mut result,
1813 app_page,
1814 not_found_tree,
1815 root_params,
1816 );
1817 }
1818
1819 if matches!(*next_mode.await?, NextMode::Build) {
1824 let next_package = get_next_package(app_dir.clone()).await?;
1827 let global_error_tree = AppPageLoaderTree {
1828 page: app_page.clone(),
1829 segment: directory_name.clone(),
1830 parallel_routes: fxindexmap! {
1831 rcstr!("children") => AppPageLoaderTree {
1832 page: app_page.clone(),
1833 segment: rcstr!("__PAGE__"),
1834 parallel_routes: FxIndexMap::default(),
1835 modules: AppDirModules {
1836 page: Some(next_package
1837 .join("dist/client/components/builtin/app-error.js")?),
1838 ..Default::default()
1839 },
1840 global_metadata,
1841 static_siblings: Vec::new(),
1842 }
1843 },
1844 modules: AppDirModules {
1847 global_error: modules.global_error.clone(),
1848 ..Default::default()
1849 },
1850 global_metadata,
1851 static_siblings: Vec::new(),
1852 }
1853 .resolved_cell();
1854
1855 let app_global_error_page = app_page
1856 .clone_push_str("_global-error")?
1857 .complete(PageType::Page)?;
1858 add_app_page(
1859 app_dir.clone(),
1860 &mut result,
1861 app_global_error_page,
1862 global_error_tree,
1863 root_params,
1864 );
1865 }
1866 }
1867
1868 let app_page = &app_page;
1869 let directory_name = &directory_name;
1870 let subdirectories = subdirectories
1871 .iter()
1872 .map(|(subdir_name, &subdirectory)| {
1873 let app_dir = app_dir.clone();
1874
1875 async move {
1876 let mut child_app_page = app_page.clone();
1877 let mut illegal_path = None;
1878
1879 if let Err(e) = child_app_page.push_str(&normalize_underscore(subdir_name)) {
1883 illegal_path = Some(e);
1884 }
1885
1886 let map = directory_tree_to_entrypoints_internal(
1887 app_dir.clone(),
1888 *global_metadata,
1889 is_global_not_found_enabled,
1890 next_mode,
1891 subdir_name.clone(),
1892 *subdirectory,
1893 child_app_page.clone(),
1894 *root_layouts,
1895 *root_params,
1896 )
1897 .await?;
1898
1899 if let Some(illegal_path) = illegal_path
1900 && !map.is_empty()
1901 {
1902 return Err(illegal_path);
1903 }
1904
1905 let mut loader_trees = Vec::new();
1906
1907 for (_, entrypoint) in map.iter() {
1908 if let Entrypoint::AppPage { ref pages, .. } = *entrypoint {
1909 for page in pages {
1910 let app_path = AppPath::from(page.clone());
1911
1912 let loader_tree = directory_tree_to_loader_tree(
1913 app_dir.clone(),
1914 *global_metadata,
1915 directory_name.clone(),
1916 directory_tree_vc,
1917 app_page.clone(),
1918 app_path,
1919 );
1920 loader_trees.push(loader_tree);
1921 }
1922 }
1923 }
1924 Ok((map, loader_trees))
1925 }
1926 })
1927 .try_join()
1928 .await?;
1929
1930 for (map, loader_trees) in subdirectories.iter() {
1931 let mut i = 0;
1932 for (_, entrypoint) in map.iter() {
1933 match entrypoint {
1934 Entrypoint::AppPage {
1935 pages,
1936 loader_tree: _,
1937 root_params,
1938 } => {
1939 for page in pages {
1940 let loader_tree = *loader_trees[i].await?;
1941 i += 1;
1942
1943 add_app_page(
1944 app_dir.clone(),
1945 &mut result,
1946 page.clone(),
1947 loader_tree
1948 .context("loader tree should be created for a page/default")?,
1949 *root_params,
1950 );
1951 }
1952 }
1953 Entrypoint::AppRoute {
1954 page,
1955 path,
1956 root_layouts,
1957 root_params,
1958 } => {
1959 add_app_route(
1960 app_dir.clone(),
1961 &mut result,
1962 page.clone(),
1963 path.clone(),
1964 *root_layouts,
1965 *root_params,
1966 );
1967 }
1968 Entrypoint::AppMetadata {
1969 page,
1970 metadata,
1971 root_params,
1972 } => {
1973 add_app_metadata_route(
1974 app_dir.clone(),
1975 &mut result,
1976 page.clone(),
1977 metadata.clone(),
1978 *root_params,
1979 );
1980 }
1981 }
1982 }
1983 }
1984 Ok(Vc::cell(result))
1985}
1986
1987#[turbo_tasks::function]
1989pub async fn get_global_metadata(
1990 app_dir: FileSystemPath,
1991 page_extensions: Vc<Vec<RcStr>>,
1992) -> Result<Vc<GlobalMetadata>> {
1993 let DirectoryContent::Entries(entries) = &*app_dir.read_dir().await? else {
1994 bail!("app_dir must be a directory")
1995 };
1996 let mut metadata = GlobalMetadata::default();
1997
1998 for (basename, entry) in entries {
1999 let DirectoryEntry::File(file) = entry else {
2000 continue;
2001 };
2002
2003 let Some(GlobalMetadataFileMatch {
2004 metadata_type,
2005 dynamic,
2006 }) = match_global_metadata_file(basename, &page_extensions.await?)
2007 else {
2008 continue;
2009 };
2010
2011 let entry = match metadata_type {
2012 "favicon" => &mut metadata.favicon,
2013 "manifest" => &mut metadata.manifest,
2014 "robots" => &mut metadata.robots,
2015 _ => continue,
2016 };
2017
2018 if dynamic {
2019 *entry = Some(MetadataItem::Dynamic { path: file.clone() });
2020 } else {
2021 *entry = Some(MetadataItem::Static { path: file.clone() });
2022 }
2023 }
2025
2026 Ok(metadata.cell())
2027}
2028
2029#[turbo_tasks::value(shared)]
2030struct DirectoryTreeIssue {
2031 pub severity: IssueSeverity,
2032 pub app_dir: FileSystemPath,
2033 pub message: ResolvedVc<StyledString>,
2034}
2035
2036#[async_trait]
2037#[turbo_tasks::value_impl]
2038impl Issue for DirectoryTreeIssue {
2039 fn severity(&self) -> IssueSeverity {
2040 self.severity
2041 }
2042
2043 async fn title(&self) -> Result<StyledString> {
2044 Ok(StyledString::Text(rcstr!(
2045 "An issue occurred while preparing your Next.js app"
2046 )))
2047 }
2048
2049 fn stage(&self) -> IssueStage {
2050 IssueStage::AppStructure
2051 }
2052
2053 async fn file_path(&self) -> Result<FileSystemPath> {
2054 Ok(self.app_dir.clone())
2055 }
2056
2057 async fn description(&self) -> Result<Option<StyledString>> {
2058 Ok(Some((*self.message.await?).clone()))
2059 }
2060}