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