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