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 MissingRootLayoutIssue {
903 app_dir: FileSystemPath,
904 page_path: FileSystemPath,
905}
906
907#[async_trait]
908#[turbo_tasks::value_impl]
909impl Issue for MissingRootLayoutIssue {
910 async fn file_path(&self) -> Result<FileSystemPath> {
911 Ok(self.page_path.clone())
912 }
913
914 fn stage(&self) -> IssueStage {
915 IssueStage::AppStructure
916 }
917
918 fn severity(&self) -> IssueSeverity {
919 IssueSeverity::Error
920 }
921
922 async fn title(&self) -> Result<StyledString> {
923 let page_path = self
924 .app_dir
925 .get_path_to(&self.page_path)
926 .context("page should be within the app directory")?;
927
928 Ok(StyledString::Text(
929 format!(
930 "{page_path} doesn't have a root layout. To fix this error, make sure every page \
931 has a root layout."
932 )
933 .into(),
934 ))
935 }
936}
937
938#[turbo_tasks::value]
939struct MissingDefaultParallelRouteIssue {
940 app_dir: FileSystemPath,
941 app_page: AppPage,
942 slot_name: RcStr,
943}
944
945#[turbo_tasks::function]
946fn missing_default_parallel_route_issue(
947 app_dir: FileSystemPath,
948 app_page: AppPage,
949 slot_name: RcStr,
950) -> Vc<MissingDefaultParallelRouteIssue> {
951 MissingDefaultParallelRouteIssue {
952 app_dir,
953 app_page,
954 slot_name,
955 }
956 .cell()
957}
958
959#[async_trait]
960#[turbo_tasks::value_impl]
961impl Issue for MissingDefaultParallelRouteIssue {
962 async fn file_path(&self) -> Result<FileSystemPath> {
963 self.app_dir
964 .join(&self.app_page.to_string())?
965 .join(&format!("@{}", self.slot_name))
966 }
967
968 fn stage(&self) -> IssueStage {
969 IssueStage::AppStructure
970 }
971
972 fn severity(&self) -> IssueSeverity {
973 IssueSeverity::Error
974 }
975
976 async fn title(&self) -> Result<StyledString> {
977 Ok(StyledString::Text(
978 format!(
979 "Missing required default.js file for parallel route at {}/@{}",
980 self.app_page, self.slot_name
981 )
982 .into(),
983 ))
984 }
985
986 async fn description(&self) -> Result<Option<StyledString>> {
987 Ok(Some(StyledString::Stack(vec![
988 StyledString::Text(
989 format!(
990 "The parallel route slot \"@{}\" is missing a default.js file. When using \
991 parallel routes, each slot must have a default.js file to serve as a \
992 fallback.",
993 self.slot_name
994 )
995 .into(),
996 ),
997 StyledString::Text(
998 format!(
999 "Create a default.js file at: {}/@{}/default.js",
1000 self.app_page, self.slot_name
1001 )
1002 .into(),
1003 ),
1004 ])))
1005 }
1006
1007 fn documentation_link(&self) -> RcStr {
1008 rcstr!("https://nextjs.org/docs/messages/slot-missing-default")
1009 }
1010}
1011
1012fn page_path_except_parallel(loader_tree: &AppPageLoaderTree) -> Option<AppPage> {
1013 if loader_tree.page.iter().any(|v| {
1014 matches!(
1015 v,
1016 PageSegment::CatchAll(..)
1017 | PageSegment::OptionalCatchAll(..)
1018 | PageSegment::Parallel(..)
1019 )
1020 }) {
1021 return None;
1022 }
1023
1024 if loader_tree.modules.page.is_some() {
1025 return Some(loader_tree.page.clone());
1026 }
1027
1028 if let Some(children) = loader_tree.parallel_routes.get("children") {
1029 return page_path_except_parallel(children);
1030 }
1031
1032 None
1033}
1034
1035fn has_child_routes(directory_tree: &PlainDirectoryTree) -> bool {
1039 for (name, subdirectory) in &directory_tree.subdirectories {
1040 if is_parallel_route(name) {
1042 continue;
1043 }
1044
1045 if is_group_route(name) {
1047 if has_child_routes(subdirectory) {
1049 return true;
1050 }
1051 continue;
1052 }
1053
1054 return true;
1056 }
1057
1058 false
1059}
1060
1061async fn check_duplicate(
1062 duplicate: &mut FxHashMap<AppPath, AppPage>,
1063 loader_tree: &AppPageLoaderTree,
1064 app_dir: FileSystemPath,
1065) -> Result<()> {
1066 let page_path = page_path_except_parallel(loader_tree);
1067
1068 if let Some(page_path) = page_path
1069 && let Some(prev) = duplicate.insert(AppPath::from(page_path.clone()), page_path.clone())
1070 && prev != page_path
1071 {
1072 DuplicateParallelRouteIssue {
1073 app_dir: app_dir.clone(),
1074 previously_inserted_page: prev.clone(),
1075 page: loader_tree.page.clone(),
1076 }
1077 .resolved_cell()
1078 .emit();
1079 }
1080
1081 Ok(())
1082}
1083
1084#[turbo_tasks::value(transparent)]
1085struct AppPageLoaderTreeOption(Option<ResolvedVc<AppPageLoaderTree>>);
1086
1087#[turbo_tasks::function]
1089async fn directory_tree_to_loader_tree(
1090 app_dir: FileSystemPath,
1091 global_metadata: Vc<GlobalMetadata>,
1092 directory_name: RcStr,
1093 directory_tree: Vc<DirectoryTree>,
1094 app_page: AppPage,
1095 for_app_path: AppPath,
1097) -> Result<Vc<AppPageLoaderTreeOption>> {
1098 let plain_tree_vc = directory_tree.into_plain();
1099 let plain_tree = &*plain_tree_vc.await?;
1100
1101 let tree = directory_tree_to_loader_tree_internal(
1102 app_dir,
1103 global_metadata,
1104 directory_name,
1105 plain_tree,
1106 app_page,
1107 for_app_path,
1108 AppDirModules::default(),
1109 Some(&plain_tree.url_tree),
1110 )
1111 .await?;
1112
1113 Ok(Vc::cell(tree.map(AppPageLoaderTree::resolved_cell)))
1114}
1115
1116async fn check_and_update_module_references(
1131 app_dir: FileSystemPath,
1132 module: &mut Option<FileSystemPath>,
1133 parent_module: &mut Option<FileSystemPath>,
1134 file_path: &str,
1135 is_first_layer_group_route: bool,
1136) -> Result<()> {
1137 match (module.as_mut(), parent_module.as_mut()) {
1138 (Some(module), _) => *parent_module = Some(module.clone()),
1140 (None, Some(parent_module)) if is_first_layer_group_route => {
1143 *module = Some(parent_module.clone())
1144 }
1145 (None, Some(_)) => {}
1148 (None, None) => {
1152 let default_page = get_next_package(app_dir).await?.join(file_path)?;
1153 *module = Some(default_page.clone());
1154 *parent_module = Some(default_page);
1155 }
1156 }
1157
1158 Ok(())
1159}
1160
1161async fn check_and_update_global_module_references(
1169 app_dir: FileSystemPath,
1170 module: &mut Option<FileSystemPath>,
1171 file_path: &str,
1172) -> Result<()> {
1173 if module.is_none() {
1174 *module = Some(get_next_package(app_dir).await?.join(file_path)?);
1175 }
1176
1177 Ok(())
1178}
1179
1180async fn directory_tree_to_loader_tree_internal(
1181 app_dir: FileSystemPath,
1182 global_metadata: Vc<GlobalMetadata>,
1183 directory_name: RcStr,
1184 directory_tree: &PlainDirectoryTree,
1185 app_page: AppPage,
1186 for_app_path: AppPath,
1188 mut parent_modules: AppDirModules,
1189 url_tree: Option<&UrlSegmentTree>,
1190) -> Result<Option<AppPageLoaderTree>> {
1191 let app_path = AppPath::from(app_page.clone());
1192
1193 if !for_app_path.contains(&app_path) {
1194 return Ok(None);
1195 }
1196
1197 let mut modules = directory_tree.modules.clone();
1198
1199 modules.metadata.base_page = Some(app_page.clone());
1202
1203 let is_root_directory = app_page.is_root();
1205
1206 let is_first_layer_group_route = app_page.is_first_layer_group_route();
1208
1209 if is_root_directory || is_first_layer_group_route {
1212 check_and_update_module_references(
1213 app_dir.clone(),
1214 &mut modules.not_found,
1215 &mut parent_modules.not_found,
1216 "dist/client/components/builtin/not-found.js",
1217 is_first_layer_group_route,
1218 )
1219 .await?;
1220
1221 check_and_update_module_references(
1222 app_dir.clone(),
1223 &mut modules.forbidden,
1224 &mut parent_modules.forbidden,
1225 "dist/client/components/builtin/forbidden.js",
1226 is_first_layer_group_route,
1227 )
1228 .await?;
1229
1230 check_and_update_module_references(
1231 app_dir.clone(),
1232 &mut modules.unauthorized,
1233 &mut parent_modules.unauthorized,
1234 "dist/client/components/builtin/unauthorized.js",
1235 is_first_layer_group_route,
1236 )
1237 .await?;
1238 }
1239
1240 if is_root_directory {
1241 check_and_update_global_module_references(
1242 app_dir.clone(),
1243 &mut modules.global_error,
1244 "dist/client/components/builtin/global-error.js",
1245 )
1246 .await?;
1247 }
1248
1249 let static_siblings: Vec<RcStr> = if is_dynamic_segment(&directory_name) {
1253 url_tree
1254 .map(|t| {
1255 t.static_children()
1256 .into_iter()
1257 .filter(|s| s != &directory_name)
1258 .collect()
1259 })
1260 .unwrap_or_default()
1261 } else {
1262 Vec::new()
1264 };
1265
1266 let mut tree = AppPageLoaderTree {
1267 page: app_page.clone(),
1268 segment: directory_name.clone(),
1269 parallel_routes: FxIndexMap::default(),
1270 modules: modules.without_leaves(),
1271 global_metadata: global_metadata.to_resolved().await?,
1272 static_siblings,
1273 };
1274
1275 let current_level_is_parallel_route = is_parallel_route(&directory_name);
1276
1277 if current_level_is_parallel_route {
1278 tree.segment = rcstr!("(__SLOT__)");
1279 }
1280
1281 if let Some(page) = (app_path == for_app_path || app_path.is_catchall())
1282 .then_some(modules.page)
1283 .flatten()
1284 {
1285 tree.parallel_routes.insert(
1286 rcstr!("children"),
1287 AppPageLoaderTree {
1288 page: app_page.clone(),
1289 segment: rcstr!("__PAGE__"),
1290 parallel_routes: FxIndexMap::default(),
1291 modules: AppDirModules {
1292 page: Some(page),
1293 metadata: modules.metadata,
1294 ..Default::default()
1295 },
1296 global_metadata: global_metadata.to_resolved().await?,
1297 static_siblings: Vec::new(),
1298 },
1299 );
1300 }
1301
1302 let mut duplicate = FxHashMap::default();
1303
1304 for (subdir_name, subdirectory) in &directory_tree.subdirectories {
1305 let parallel_route_key = match_parallel_route(subdir_name);
1306
1307 let mut child_app_page = app_page.clone();
1308 let mut illegal_path_error = None;
1309
1310 if let Err(e) = child_app_page.push_str(&normalize_underscore(subdir_name)) {
1314 illegal_path_error = Some(e);
1315 }
1316
1317 let child_url_tree: Option<&UrlSegmentTree> =
1319 if directory_name.is_empty() || is_url_transparent_segment(&directory_name) {
1320 url_tree
1321 } else {
1322 url_tree.and_then(|t| t.get_child(&directory_name))
1323 };
1324
1325 let subtree = Box::pin(directory_tree_to_loader_tree_internal(
1326 app_dir.clone(),
1327 global_metadata,
1328 subdir_name.clone(),
1329 subdirectory,
1330 child_app_page.clone(),
1331 for_app_path.clone(),
1332 parent_modules.clone(),
1333 child_url_tree,
1334 ))
1335 .await?;
1336
1337 if let Some(illegal_path) = subtree.as_ref().and(illegal_path_error) {
1338 return Err(illegal_path);
1339 }
1340
1341 if let Some(subtree) = subtree {
1342 if let Some(key) = parallel_route_key {
1343 let is_inside_catchall = app_page.is_catchall();
1354
1355 let is_leaf_segment = !has_child_routes(directory_tree);
1363
1364 let slot_has_children = has_child_routes(subdirectory);
1375
1376 if key != "children"
1377 && subdirectory.modules.default.is_none()
1378 && !is_inside_catchall
1379 && !is_leaf_segment
1380 && !slot_has_children
1381 {
1382 missing_default_parallel_route_issue(
1383 app_dir.clone(),
1384 app_page.clone(),
1385 key.into(),
1386 )
1387 .to_resolved()
1388 .await?
1389 .emit();
1390 }
1391
1392 tree.parallel_routes.insert(key.into(), subtree);
1393 continue;
1394 }
1395
1396 if is_group_route(subdir_name) && !subtree.has_page() {
1398 continue;
1399 }
1400
1401 if subtree.has_page() {
1402 check_duplicate(&mut duplicate, &subtree, app_dir.clone()).await?;
1403 }
1404
1405 if let Some(current_tree) = tree.parallel_routes.get("children") {
1406 if current_tree.has_only_catchall()
1407 && (!subtree.has_only_catchall()
1408 || current_tree.get_specificity() < subtree.get_specificity())
1409 {
1410 tree.parallel_routes
1411 .insert(rcstr!("children"), subtree.clone());
1412 }
1413 } else {
1414 tree.parallel_routes.insert(rcstr!("children"), subtree);
1415 }
1416 } else if let Some(key) = parallel_route_key {
1417 bail!(
1418 "missing page or default for parallel route `{}` (page: {})",
1419 key,
1420 app_page
1421 );
1422 }
1423 }
1424
1425 if tree
1428 .parallel_routes
1429 .iter()
1430 .any(|(_, parallel_tree)| parallel_tree.is_intercepting())
1431 {
1432 let mut keys_to_replace = Vec::new();
1433
1434 for (key, parallel_tree) in &tree.parallel_routes {
1435 if !parallel_tree.is_intercepting() {
1436 keys_to_replace.push(key.clone());
1437 }
1438 }
1439
1440 for key in keys_to_replace {
1441 let subdir_name: RcStr = format!("@{key}").into();
1442
1443 let default = if key == "children" {
1444 modules.default.clone()
1445 } else if let Some(subdirectory) = directory_tree.subdirectories.get(&subdir_name) {
1446 subdirectory.modules.default.clone()
1447 } else {
1448 None
1449 };
1450
1451 let is_inside_catchall = app_page.is_catchall();
1452
1453 let is_leaf_segment = !has_child_routes(directory_tree);
1455
1456 if default.is_none() && key != "children" && !is_inside_catchall && !is_leaf_segment {
1461 missing_default_parallel_route_issue(
1462 app_dir.clone(),
1463 app_page.clone(),
1464 key.clone(),
1465 )
1466 .to_resolved()
1467 .await?
1468 .emit();
1469 }
1470
1471 tree.parallel_routes.insert(
1472 key.clone(),
1473 default_route_tree(
1474 app_dir.clone(),
1475 global_metadata,
1476 app_page.clone(),
1477 default,
1478 key.clone(),
1479 for_app_path.clone(),
1480 )
1481 .await?,
1482 );
1483 }
1484 }
1485
1486 if tree.parallel_routes.is_empty() {
1487 if modules.default.is_some() || current_level_is_parallel_route {
1488 tree = default_route_tree(
1489 app_dir.clone(),
1490 global_metadata,
1491 app_page.clone(),
1492 modules.default.clone(),
1493 rcstr!("children"),
1494 for_app_path.clone(),
1495 )
1496 .await?;
1497 } else {
1498 return Ok(None);
1499 }
1500 } else if tree.parallel_routes.get("children").is_none() {
1501 tree.parallel_routes.insert(
1502 rcstr!("children"),
1503 default_route_tree(
1504 app_dir.clone(),
1505 global_metadata,
1506 app_page.clone(),
1507 modules.default.clone(),
1508 rcstr!("children"),
1509 for_app_path.clone(),
1510 )
1511 .await?,
1512 );
1513 }
1514
1515 Ok(Some(tree))
1516}
1517
1518async fn default_route_tree(
1519 app_dir: FileSystemPath,
1520 global_metadata: Vc<GlobalMetadata>,
1521 app_page: AppPage,
1522 default_component: Option<FileSystemPath>,
1523 slot_name: RcStr,
1524 for_app_path: AppPath,
1525) -> Result<AppPageLoaderTree> {
1526 Ok(AppPageLoaderTree {
1527 page: app_page.clone(),
1528 segment: rcstr!("__DEFAULT__"),
1529 parallel_routes: FxIndexMap::default(),
1530 modules: if let Some(default) = default_component {
1531 AppDirModules {
1532 default: Some(default),
1533 ..Default::default()
1534 }
1535 } else {
1536 let contains_interception = for_app_path.contains_interception();
1537
1538 let default_file = if contains_interception && slot_name == "children" {
1539 "dist/client/components/builtin/default-null.js"
1540 } else {
1541 "dist/client/components/builtin/default.js"
1542 };
1543
1544 AppDirModules {
1545 default: Some(get_next_package(app_dir).await?.join(default_file)?),
1546 ..Default::default()
1547 }
1548 },
1549 global_metadata: global_metadata.to_resolved().await?,
1550 static_siblings: Vec::new(),
1551 })
1552}
1553
1554#[turbo_tasks::function]
1555async fn directory_tree_to_entrypoints_internal(
1556 app_dir: FileSystemPath,
1557 global_metadata: ResolvedVc<GlobalMetadata>,
1558 is_global_not_found_enabled: Vc<bool>,
1559 next_mode: Vc<NextMode>,
1560 directory_name: RcStr,
1561 directory_tree: Vc<DirectoryTree>,
1562 app_page: AppPage,
1563 root_layouts: ResolvedVc<FileSystemPathVec>,
1564 root_params: ResolvedVc<RootParamVecOption>,
1565) -> Result<Vc<Entrypoints>> {
1566 let span = tracing::info_span!("build layout trees", name = display(&app_page));
1567 directory_tree_to_entrypoints_internal_untraced(
1568 app_dir,
1569 global_metadata,
1570 is_global_not_found_enabled,
1571 next_mode,
1572 directory_name,
1573 directory_tree,
1574 app_page,
1575 root_layouts,
1576 root_params,
1577 )
1578 .instrument(span)
1579 .await
1580}
1581
1582async fn directory_tree_to_entrypoints_internal_untraced(
1583 app_dir: FileSystemPath,
1584 global_metadata: ResolvedVc<GlobalMetadata>,
1585 is_global_not_found_enabled: Vc<bool>,
1586 next_mode: Vc<NextMode>,
1587 directory_name: RcStr,
1588 directory_tree: Vc<DirectoryTree>,
1589 app_page: AppPage,
1590 root_layouts: ResolvedVc<FileSystemPathVec>,
1591 root_params: ResolvedVc<RootParamVecOption>,
1592) -> Result<Vc<Entrypoints>> {
1593 let mut result = FxIndexMap::default();
1594
1595 let directory_tree_vc = directory_tree;
1596 let directory_tree = &*directory_tree.await?;
1597
1598 let subdirectories = &directory_tree.subdirectories;
1599 let modules = &directory_tree.modules;
1600 let root_layouts = if let Some(layout) = &modules.layout {
1604 let mut layouts = root_layouts.owned().await?;
1605 layouts.push(layout.clone());
1606 ResolvedVc::cell(layouts)
1607 } else {
1608 root_layouts
1609 };
1610
1611 let root_params = if root_params.await?.is_none() && (*root_layouts.await?).len() == 1 {
1613 ResolvedVc::cell(Some(
1616 app_page
1617 .0
1618 .iter()
1619 .filter_map(|segment| match segment {
1620 PageSegment::Dynamic(param)
1621 | PageSegment::CatchAll(param)
1622 | PageSegment::OptionalCatchAll(param) => Some(param.clone()),
1623 _ => None,
1624 })
1625 .collect::<Vec<RcStr>>(),
1626 ))
1627 } else {
1628 root_params
1629 };
1630
1631 if let Some(page_path) = &modules.page {
1632 if root_layouts.await?.is_empty() {
1633 MissingRootLayoutIssue {
1634 app_dir: app_dir.clone(),
1635 page_path: page_path.clone(),
1636 }
1637 .resolved_cell()
1638 .emit();
1639 }
1640
1641 let app_path = AppPath::from(app_page.clone());
1642
1643 let loader_tree = *directory_tree_to_loader_tree(
1644 app_dir.clone(),
1645 *global_metadata,
1646 directory_name.clone(),
1647 directory_tree_vc,
1648 app_page.clone(),
1649 app_path,
1650 )
1651 .await?;
1652
1653 add_app_page(
1654 app_dir.clone(),
1655 &mut result,
1656 app_page.complete(PageType::Page)?,
1657 loader_tree.context("loader tree should be created for a page/default")?,
1658 root_params,
1659 );
1660 }
1661
1662 if let Some(route) = &modules.route {
1663 add_app_route(
1664 app_dir.clone(),
1665 &mut result,
1666 app_page.complete(PageType::Route)?,
1667 route.clone(),
1668 root_layouts,
1669 root_params,
1670 );
1671 }
1672
1673 let Metadata {
1674 icon,
1675 apple,
1676 twitter,
1677 open_graph,
1678 sitemap,
1679 base_page: _,
1680 } = &modules.metadata;
1681
1682 for meta in sitemap
1683 .iter()
1684 .cloned()
1685 .chain(icon.iter().cloned().map(MetadataItem::from))
1686 .chain(apple.iter().cloned().map(MetadataItem::from))
1687 .chain(twitter.iter().cloned().map(MetadataItem::from))
1688 .chain(open_graph.iter().cloned().map(MetadataItem::from))
1689 {
1690 let app_page = app_page.clone_push_str(&get_metadata_route_name(meta.clone()).await?)?;
1691
1692 add_app_metadata_route(
1693 app_dir.clone(),
1694 &mut result,
1695 normalize_metadata_route(app_page)?,
1696 meta,
1697 root_params,
1698 );
1699 }
1700
1701 if app_page.is_root() {
1703 let GlobalMetadata {
1704 favicon,
1705 robots,
1706 manifest,
1707 } = &*global_metadata.await?;
1708
1709 for meta in favicon.iter().chain(robots.iter()).chain(manifest.iter()) {
1710 let app_page =
1711 app_page.clone_push_str(&get_metadata_route_name(meta.clone()).await?)?;
1712
1713 add_app_metadata_route(
1714 app_dir.clone(),
1715 &mut result,
1716 normalize_metadata_route(app_page)?,
1717 meta.clone(),
1718 root_params,
1719 );
1720 }
1721
1722 let mut modules = directory_tree.modules.clone();
1723
1724 if modules.layout.is_none() {
1726 modules.layout = Some(
1727 get_next_package(app_dir.clone())
1728 .await?
1729 .join("dist/client/components/builtin/layout.js")?,
1730 );
1731 }
1732
1733 if modules.not_found.is_none() {
1734 modules.not_found = Some(
1735 get_next_package(app_dir.clone())
1736 .await?
1737 .join("dist/client/components/builtin/not-found.js")?,
1738 );
1739 }
1740 if modules.forbidden.is_none() {
1741 modules.forbidden = Some(
1742 get_next_package(app_dir.clone())
1743 .await?
1744 .join("dist/client/components/builtin/forbidden.js")?,
1745 );
1746 }
1747 if modules.unauthorized.is_none() {
1748 modules.unauthorized = Some(
1749 get_next_package(app_dir.clone())
1750 .await?
1751 .join("dist/client/components/builtin/unauthorized.js")?,
1752 );
1753 }
1754 if modules.global_error.is_none() {
1755 modules.global_error = Some(
1756 get_next_package(app_dir.clone())
1757 .await?
1758 .join("dist/client/components/builtin/global-error.js")?,
1759 );
1760 }
1761
1762 let is_global_not_found_enabled = *is_global_not_found_enabled.await?;
1767 let use_global_not_found =
1768 is_global_not_found_enabled || modules.global_not_found.is_some();
1769
1770 let not_found_root_modules = modules.without_leaves();
1771 let not_found_tree = AppPageLoaderTree {
1772 page: app_page.clone(),
1773 segment: directory_name.clone(),
1774 parallel_routes: fxindexmap! {
1775 rcstr!("children") => AppPageLoaderTree {
1776 page: app_page.clone(),
1777 segment: rcstr!("/_not-found"),
1778 parallel_routes: fxindexmap! {
1779 rcstr!("children") => AppPageLoaderTree {
1780 page: app_page.clone(),
1781 segment: rcstr!("__PAGE__"),
1782 parallel_routes: FxIndexMap::default(),
1783 modules: if use_global_not_found {
1784 AppDirModules {
1787 page: Some(get_next_package(app_dir.clone())
1789 .await?
1790 .join("dist/client/components/builtin/empty-stub.js")?,
1791 ),
1792 ..Default::default()
1793 }
1794 } else {
1795 AppDirModules {
1798 page: match modules.not_found {
1799 Some(v) => Some(v),
1800 None => Some(get_next_package(app_dir.clone())
1801 .await?
1802 .join("dist/client/components/builtin/not-found.js")?,
1803 ),
1804 },
1805 ..Default::default()
1806 }
1807 },
1808 global_metadata,
1809 static_siblings: Vec::new(),
1810 }
1811 },
1812 modules: AppDirModules {
1813 ..Default::default()
1814 },
1815 global_metadata,
1816 static_siblings: Vec::new(),
1817 },
1818 },
1819 modules: AppDirModules {
1820 layout: if use_global_not_found {
1825 match modules.global_not_found {
1826 Some(v) => Some(v),
1827 None => Some(
1828 get_next_package(app_dir.clone())
1829 .await?
1830 .join("dist/client/components/builtin/global-not-found.js")?,
1831 ),
1832 }
1833 } else {
1834 modules.layout
1835 },
1836 ..not_found_root_modules
1837 },
1838 global_metadata,
1839 static_siblings: Vec::new(),
1840 }
1841 .resolved_cell();
1842
1843 {
1844 let app_page = app_page
1845 .clone_push_str("_not-found")?
1846 .complete(PageType::Page)?;
1847
1848 add_app_page(
1849 app_dir.clone(),
1850 &mut result,
1851 app_page,
1852 not_found_tree,
1853 root_params,
1854 );
1855 }
1856
1857 if matches!(*next_mode.await?, NextMode::Build) {
1862 let next_package = get_next_package(app_dir.clone()).await?;
1865 let global_error_tree = AppPageLoaderTree {
1866 page: app_page.clone(),
1867 segment: directory_name.clone(),
1868 parallel_routes: fxindexmap! {
1869 rcstr!("children") => AppPageLoaderTree {
1870 page: app_page.clone(),
1871 segment: rcstr!("__PAGE__"),
1872 parallel_routes: FxIndexMap::default(),
1873 modules: AppDirModules {
1874 page: Some(next_package
1875 .join("dist/client/components/builtin/app-error.js")?),
1876 ..Default::default()
1877 },
1878 global_metadata,
1879 static_siblings: Vec::new(),
1880 }
1881 },
1882 modules: AppDirModules {
1885 global_error: modules.global_error.clone(),
1886 ..Default::default()
1887 },
1888 global_metadata,
1889 static_siblings: Vec::new(),
1890 }
1891 .resolved_cell();
1892
1893 let app_global_error_page = app_page
1894 .clone_push_str("_global-error")?
1895 .complete(PageType::Page)?;
1896 add_app_page(
1897 app_dir.clone(),
1898 &mut result,
1899 app_global_error_page,
1900 global_error_tree,
1901 root_params,
1902 );
1903 }
1904 }
1905
1906 let app_page = &app_page;
1907 let directory_name = &directory_name;
1908 let subdirectories = subdirectories
1909 .iter()
1910 .map(|(subdir_name, &subdirectory)| {
1911 let app_dir = app_dir.clone();
1912
1913 async move {
1914 let mut child_app_page = app_page.clone();
1915 let mut illegal_path = None;
1916
1917 if let Err(e) = child_app_page.push_str(&normalize_underscore(subdir_name)) {
1921 illegal_path = Some(e);
1922 }
1923
1924 let map = directory_tree_to_entrypoints_internal(
1925 app_dir.clone(),
1926 *global_metadata,
1927 is_global_not_found_enabled,
1928 next_mode,
1929 subdir_name.clone(),
1930 *subdirectory,
1931 child_app_page.clone(),
1932 *root_layouts,
1933 *root_params,
1934 )
1935 .await?;
1936
1937 if let Some(illegal_path) = illegal_path
1938 && !map.is_empty()
1939 {
1940 return Err(illegal_path);
1941 }
1942
1943 let mut loader_trees = Vec::new();
1944
1945 for (_, entrypoint) in map.iter() {
1946 if let Entrypoint::AppPage { ref pages, .. } = *entrypoint {
1947 for page in pages {
1948 let app_path = AppPath::from(page.clone());
1949
1950 let loader_tree = directory_tree_to_loader_tree(
1951 app_dir.clone(),
1952 *global_metadata,
1953 directory_name.clone(),
1954 directory_tree_vc,
1955 app_page.clone(),
1956 app_path,
1957 );
1958 loader_trees.push(loader_tree);
1959 }
1960 }
1961 }
1962 Ok((map, loader_trees))
1963 }
1964 })
1965 .try_join()
1966 .await?;
1967
1968 for (map, loader_trees) in subdirectories.iter() {
1969 let mut i = 0;
1970 for (_, entrypoint) in map.iter() {
1971 match entrypoint {
1972 Entrypoint::AppPage {
1973 pages,
1974 loader_tree: _,
1975 root_params,
1976 } => {
1977 for page in pages {
1978 let loader_tree = *loader_trees[i].await?;
1979 i += 1;
1980
1981 add_app_page(
1982 app_dir.clone(),
1983 &mut result,
1984 page.clone(),
1985 loader_tree
1986 .context("loader tree should be created for a page/default")?,
1987 *root_params,
1988 );
1989 }
1990 }
1991 Entrypoint::AppRoute {
1992 page,
1993 path,
1994 root_layouts,
1995 root_params,
1996 } => {
1997 add_app_route(
1998 app_dir.clone(),
1999 &mut result,
2000 page.clone(),
2001 path.clone(),
2002 *root_layouts,
2003 *root_params,
2004 );
2005 }
2006 Entrypoint::AppMetadata {
2007 page,
2008 metadata,
2009 root_params,
2010 } => {
2011 add_app_metadata_route(
2012 app_dir.clone(),
2013 &mut result,
2014 page.clone(),
2015 metadata.clone(),
2016 *root_params,
2017 );
2018 }
2019 }
2020 }
2021 }
2022 Ok(Vc::cell(result))
2023}
2024
2025#[turbo_tasks::function]
2027pub async fn get_global_metadata(
2028 app_dir: FileSystemPath,
2029 page_extensions: Vc<Vec<RcStr>>,
2030) -> Result<Vc<GlobalMetadata>> {
2031 let DirectoryContent::Entries(entries) = &*app_dir.read_dir().await? else {
2032 bail!("app_dir must be a directory")
2033 };
2034 let mut metadata = GlobalMetadata::default();
2035
2036 for (basename, entry) in entries {
2037 let DirectoryEntry::File(file) = entry else {
2038 continue;
2039 };
2040
2041 let Some(GlobalMetadataFileMatch {
2042 metadata_type,
2043 dynamic,
2044 }) = match_global_metadata_file(basename, &page_extensions.await?)
2045 else {
2046 continue;
2047 };
2048
2049 let entry = match metadata_type {
2050 "favicon" => &mut metadata.favicon,
2051 "manifest" => &mut metadata.manifest,
2052 "robots" => &mut metadata.robots,
2053 _ => continue,
2054 };
2055
2056 if dynamic {
2057 *entry = Some(MetadataItem::Dynamic { path: file.clone() });
2058 } else {
2059 *entry = Some(MetadataItem::Static { path: file.clone() });
2060 }
2061 }
2063
2064 Ok(metadata.cell())
2065}
2066
2067#[turbo_tasks::value(shared)]
2068struct DirectoryTreeIssue {
2069 pub severity: IssueSeverity,
2070 pub app_dir: FileSystemPath,
2071 pub message: ResolvedVc<StyledString>,
2072}
2073
2074#[async_trait]
2075#[turbo_tasks::value_impl]
2076impl Issue for DirectoryTreeIssue {
2077 fn severity(&self) -> IssueSeverity {
2078 self.severity
2079 }
2080
2081 async fn title(&self) -> Result<StyledString> {
2082 Ok(StyledString::Text(rcstr!(
2083 "An issue occurred while preparing your Next.js app"
2084 )))
2085 }
2086
2087 fn stage(&self) -> IssueStage {
2088 IssueStage::AppStructure
2089 }
2090
2091 async fn file_path(&self) -> Result<FileSystemPath> {
2092 Ok(self.app_dir.clone())
2093 }
2094
2095 async fn description(&self) -> Result<Option<StyledString>> {
2096 Ok(Some((*self.message.await?).clone()))
2097 }
2098}