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