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