1use std::collections::BTreeMap;
2
3use anyhow::{Context, Result, bail};
4use indexmap::map::{Entry, OccupiedEntry};
5use rustc_hash::FxHashMap;
6use serde::{Deserialize, Serialize};
7use tracing::Instrument;
8use turbo_rcstr::RcStr;
9use turbo_tasks::{
10 FxIndexMap, NonLocalValue, ResolvedVc, TaskInput, TryJoinIterExt, ValueDefault, ValueToString,
11 Vc, 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 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
29#[turbo_tasks::value]
31#[derive(Default, Debug, Clone)]
32pub struct AppDirModules {
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub page: Option<ResolvedVc<FileSystemPath>>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub layout: Option<ResolvedVc<FileSystemPath>>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub error: Option<ResolvedVc<FileSystemPath>>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub global_error: Option<ResolvedVc<FileSystemPath>>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub global_not_found: Option<ResolvedVc<FileSystemPath>>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub loading: Option<ResolvedVc<FileSystemPath>>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub template: Option<ResolvedVc<FileSystemPath>>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub forbidden: Option<ResolvedVc<FileSystemPath>>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub unauthorized: Option<ResolvedVc<FileSystemPath>>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub not_found: Option<ResolvedVc<FileSystemPath>>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub default: Option<ResolvedVc<FileSystemPath>>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub route: Option<ResolvedVc<FileSystemPath>>,
57 #[serde(skip_serializing_if = "Metadata::is_empty", default)]
58 pub metadata: Metadata,
59}
60
61impl AppDirModules {
62 fn without_leafs(&self) -> Self {
63 Self {
64 page: None,
65 layout: self.layout,
66 error: self.error,
67 global_error: self.global_error,
68 global_not_found: self.global_not_found,
69 loading: self.loading,
70 template: self.template,
71 not_found: self.not_found,
72 forbidden: self.forbidden,
73 unauthorized: self.unauthorized,
74 default: None,
75 route: None,
76 metadata: self.metadata.clone(),
77 }
78 }
79}
80
81#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, TraceRawVcs, NonLocalValue)]
83pub enum MetadataWithAltItem {
84 Static {
85 path: ResolvedVc<FileSystemPath>,
86 alt_path: Option<ResolvedVc<FileSystemPath>>,
87 },
88 Dynamic {
89 path: ResolvedVc<FileSystemPath>,
90 },
91}
92
93#[derive(
95 Copy,
96 Clone,
97 Debug,
98 Hash,
99 Serialize,
100 Deserialize,
101 PartialEq,
102 Eq,
103 TaskInput,
104 TraceRawVcs,
105 NonLocalValue,
106)]
107pub enum MetadataItem {
108 Static { path: ResolvedVc<FileSystemPath> },
109 Dynamic { path: ResolvedVc<FileSystemPath> },
110}
111
112#[turbo_tasks::function]
113pub async fn get_metadata_route_name(meta: MetadataItem) -> Result<Vc<RcStr>> {
114 Ok(match meta {
115 MetadataItem::Static { path } => {
116 let path_value = path.await?;
117 Vc::cell(path_value.file_name().into())
118 }
119 MetadataItem::Dynamic { path } => {
120 let Some(stem) = &*path.file_stem().await? else {
121 bail!(
122 "unable to resolve file stem for metadata item at {}",
123 path.to_string().await?
124 );
125 };
126
127 match stem.as_str() {
128 "manifest" => Vc::cell("manifest.webmanifest".into()),
129 _ => Vc::cell(stem.clone()),
130 }
131 }
132 })
133}
134
135impl MetadataItem {
136 pub fn into_path(self) -> ResolvedVc<FileSystemPath> {
137 match self {
138 MetadataItem::Static { path } => path,
139 MetadataItem::Dynamic { path } => path,
140 }
141 }
142}
143
144impl From<MetadataWithAltItem> for MetadataItem {
145 fn from(value: MetadataWithAltItem) -> Self {
146 match value {
147 MetadataWithAltItem::Static { path, .. } => MetadataItem::Static { path },
148 MetadataWithAltItem::Dynamic { path } => MetadataItem::Dynamic { path },
149 }
150 }
151}
152
153#[derive(
155 Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, TraceRawVcs, NonLocalValue,
156)]
157pub struct Metadata {
158 #[serde(skip_serializing_if = "Vec::is_empty", default)]
159 pub icon: Vec<MetadataWithAltItem>,
160 #[serde(skip_serializing_if = "Vec::is_empty", default)]
161 pub apple: Vec<MetadataWithAltItem>,
162 #[serde(skip_serializing_if = "Vec::is_empty", default)]
163 pub twitter: Vec<MetadataWithAltItem>,
164 #[serde(skip_serializing_if = "Vec::is_empty", default)]
165 pub open_graph: Vec<MetadataWithAltItem>,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub sitemap: Option<MetadataItem>,
168 #[serde(skip_serializing_if = "Option::is_none")]
176 pub base_page: Option<AppPage>,
177}
178
179impl Metadata {
180 pub fn is_empty(&self) -> bool {
181 let Metadata {
182 icon,
183 apple,
184 twitter,
185 open_graph,
186 sitemap,
187 base_page: _,
188 } = self;
189 icon.is_empty()
190 && apple.is_empty()
191 && twitter.is_empty()
192 && open_graph.is_empty()
193 && sitemap.is_none()
194 }
195}
196
197#[turbo_tasks::value]
199#[derive(Default, Clone, Debug)]
200pub struct GlobalMetadata {
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub favicon: Option<MetadataItem>,
203 #[serde(skip_serializing_if = "Option::is_none")]
204 pub robots: Option<MetadataItem>,
205 #[serde(skip_serializing_if = "Option::is_none")]
206 pub manifest: Option<MetadataItem>,
207}
208
209impl GlobalMetadata {
210 pub fn is_empty(&self) -> bool {
211 let GlobalMetadata {
212 favicon,
213 robots,
214 manifest,
215 } = self;
216 favicon.is_none() && robots.is_none() && manifest.is_none()
217 }
218}
219
220#[turbo_tasks::value]
221#[derive(Debug)]
222pub struct DirectoryTree {
223 pub subdirectories: BTreeMap<RcStr, ResolvedVc<DirectoryTree>>,
225 pub modules: AppDirModules,
226}
227
228#[turbo_tasks::value]
229#[derive(Clone, Debug)]
230struct PlainDirectoryTree {
231 pub subdirectories: BTreeMap<RcStr, PlainDirectoryTree>,
233 pub modules: AppDirModules,
234}
235
236#[turbo_tasks::value_impl]
237impl DirectoryTree {
238 #[turbo_tasks::function]
239 pub async fn into_plain(&self) -> Result<Vc<PlainDirectoryTree>> {
240 let mut subdirectories = BTreeMap::new();
241
242 for (name, subdirectory) in &self.subdirectories {
243 subdirectories.insert(name.clone(), subdirectory.into_plain().owned().await?);
244 }
245
246 Ok(PlainDirectoryTree {
247 subdirectories,
248 modules: self.modules.clone(),
249 }
250 .cell())
251 }
252}
253
254#[turbo_tasks::value(transparent)]
255pub struct OptionAppDir(Option<ResolvedVc<FileSystemPath>>);
256
257#[turbo_tasks::function]
259pub async fn find_app_dir(project_path: Vc<FileSystemPath>) -> Result<Vc<OptionAppDir>> {
260 let app = project_path.join("app".into());
261 let src_app = project_path.join("src/app".into());
262 let app_dir = if *app.get_type().await? == FileSystemEntryType::Directory {
263 app
264 } else if *src_app.get_type().await? == FileSystemEntryType::Directory {
265 src_app
266 } else {
267 return Ok(Vc::cell(None));
268 }
269 .to_resolved()
270 .await?;
271
272 Ok(Vc::cell(Some(app_dir)))
273}
274
275#[turbo_tasks::function]
276async fn get_directory_tree(
277 dir: Vc<FileSystemPath>,
278 page_extensions: Vc<Vec<RcStr>>,
279) -> Result<Vc<DirectoryTree>> {
280 let span = {
281 let dir = dir.to_string().await?.to_string();
282 tracing::info_span!("read app directory tree", name = dir)
283 };
284 get_directory_tree_internal(dir, page_extensions)
285 .instrument(span)
286 .await
287}
288
289async fn get_directory_tree_internal(
290 dir: Vc<FileSystemPath>,
291 page_extensions: Vc<Vec<RcStr>>,
292) -> Result<Vc<DirectoryTree>> {
293 let DirectoryContent::Entries(entries) = &*dir.read_dir().await? else {
294 return Ok(DirectoryTree {
298 subdirectories: Default::default(),
299 modules: AppDirModules::default(),
300 }
301 .cell());
302 };
303 let page_extensions_value = page_extensions.await?;
304
305 let mut subdirectories = BTreeMap::new();
306 let mut modules = AppDirModules::default();
307
308 let mut metadata_icon = Vec::new();
309 let mut metadata_apple = Vec::new();
310 let mut metadata_open_graph = Vec::new();
311 let mut metadata_twitter = Vec::new();
312
313 for (basename, entry) in entries {
314 let entry = entry.resolve_symlink().await?;
315 match entry {
316 DirectoryEntry::File(file) => {
317 if basename.ends_with(".d.ts") {
319 continue;
320 }
321 if let Some((stem, ext)) = basename.split_once('.') {
322 if page_extensions_value.iter().any(|e| e == ext) {
323 match stem {
324 "page" => modules.page = Some(file),
325 "layout" => modules.layout = Some(file),
326 "error" => modules.error = Some(file),
327 "global-error" => modules.global_error = Some(file),
328 "global-not-found" => modules.global_not_found = Some(file),
329 "loading" => modules.loading = Some(file),
330 "template" => modules.template = Some(file),
331 "forbidden" => modules.forbidden = Some(file),
332 "unauthorized" => modules.unauthorized = Some(file),
333 "not-found" => modules.not_found = Some(file),
334 "default" => modules.default = Some(file),
335 "route" => modules.route = Some(file),
336 _ => {}
337 }
338 }
339 }
340
341 let Some(MetadataFileMatch {
342 metadata_type,
343 number,
344 dynamic,
345 }) = match_local_metadata_file(basename.as_str(), &page_extensions_value)
346 else {
347 continue;
348 };
349
350 let entry = match metadata_type {
351 "icon" => &mut metadata_icon,
352 "apple-icon" => &mut metadata_apple,
353 "twitter-image" => &mut metadata_twitter,
354 "opengraph-image" => &mut metadata_open_graph,
355 "sitemap" => {
356 if dynamic {
357 modules.metadata.sitemap = Some(MetadataItem::Dynamic { path: file });
358 } else {
359 modules.metadata.sitemap = Some(MetadataItem::Static { path: file });
360 }
361 continue;
362 }
363 _ => continue,
364 };
365
366 if dynamic {
367 entry.push((number, MetadataWithAltItem::Dynamic { path: file }));
368 continue;
369 }
370
371 let file_value = file.await?;
372 let file_name = file_value.file_name();
373 let basename = file_name
374 .rsplit_once('.')
375 .map_or(file_name, |(basename, _)| basename);
376 let alt_path = file
377 .parent()
378 .join(format!("{basename}.alt.txt").into())
379 .to_resolved()
380 .await?;
381 let alt_path = matches!(&*alt_path.get_type().await?, FileSystemEntryType::File)
382 .then_some(alt_path);
383
384 entry.push((
385 number,
386 MetadataWithAltItem::Static {
387 path: file,
388 alt_path,
389 },
390 ));
391 }
392 DirectoryEntry::Directory(dir) => {
393 if !basename.starts_with('_') {
395 let result = get_directory_tree(*dir, page_extensions)
396 .to_resolved()
397 .await?;
398 subdirectories.insert(basename.clone(), result);
399 }
400 }
401 _ => {}
403 }
404 }
405
406 fn sort<T>(mut list: Vec<(Option<u32>, T)>) -> Vec<T> {
407 list.sort_by_key(|(num, _)| *num);
408 list.into_iter().map(|(_, item)| item).collect()
409 }
410
411 modules.metadata.icon = sort(metadata_icon);
412 modules.metadata.apple = sort(metadata_apple);
413 modules.metadata.twitter = sort(metadata_twitter);
414 modules.metadata.open_graph = sort(metadata_open_graph);
415
416 Ok(DirectoryTree {
417 subdirectories,
418 modules,
419 }
420 .cell())
421}
422
423#[turbo_tasks::value]
424#[derive(Debug, Clone)]
425pub struct AppPageLoaderTree {
426 pub page: AppPage,
427 pub segment: RcStr,
428 pub parallel_routes: FxIndexMap<RcStr, AppPageLoaderTree>,
429 pub modules: AppDirModules,
430 pub global_metadata: ResolvedVc<GlobalMetadata>,
431}
432
433impl AppPageLoaderTree {
434 pub fn has_page(&self) -> bool {
436 if &*self.segment == "__PAGE__" {
437 return true;
438 }
439
440 for (_, tree) in &self.parallel_routes {
441 if tree.has_page() {
442 return true;
443 }
444 }
445
446 false
447 }
448
449 pub fn has_only_catchall(&self) -> bool {
452 if &*self.segment == "__PAGE__" && !self.page.is_catchall() {
453 return false;
454 }
455
456 for (_, tree) in &self.parallel_routes {
457 if !tree.has_only_catchall() {
458 return false;
459 }
460 }
461
462 true
463 }
464
465 pub fn is_intercepting(&self) -> bool {
467 if self.page.is_intercepting() && self.has_page() {
468 return true;
469 }
470
471 for (_, tree) in &self.parallel_routes {
472 if tree.is_intercepting() {
473 return true;
474 }
475 }
476
477 false
478 }
479
480 pub fn get_specificity(&self) -> usize {
483 if &*self.segment == "__PAGE__" {
484 return AppPath::from(self.page.clone()).len();
485 }
486
487 let mut specificity = 0;
488
489 for (_, tree) in &self.parallel_routes {
490 specificity = specificity.max(tree.get_specificity());
491 }
492
493 specificity
494 }
495}
496
497#[turbo_tasks::value(transparent)]
498pub struct FileSystemPathVec(Vec<ResolvedVc<FileSystemPath>>);
499
500#[turbo_tasks::value_impl]
501impl ValueDefault for FileSystemPathVec {
502 #[turbo_tasks::function]
503 fn value_default() -> Vc<Self> {
504 Vc::cell(Vec::new())
505 }
506}
507
508#[derive(
509 Clone,
510 PartialEq,
511 Eq,
512 Hash,
513 Serialize,
514 Deserialize,
515 TraceRawVcs,
516 ValueDebugFormat,
517 Debug,
518 TaskInput,
519 NonLocalValue,
520)]
521pub enum Entrypoint {
522 AppPage {
523 pages: Vec<AppPage>,
524 loader_tree: ResolvedVc<AppPageLoaderTree>,
525 },
526 AppRoute {
527 page: AppPage,
528 path: ResolvedVc<FileSystemPath>,
529 root_layouts: ResolvedVc<FileSystemPathVec>,
530 },
531 AppMetadata {
532 page: AppPage,
533 metadata: MetadataItem,
534 },
535}
536
537impl Entrypoint {
538 pub fn page(&self) -> &AppPage {
539 match self {
540 Entrypoint::AppPage { pages, .. } => pages.first().unwrap(),
541 Entrypoint::AppRoute { page, .. } => page,
542 Entrypoint::AppMetadata { page, .. } => page,
543 }
544 }
545}
546
547#[turbo_tasks::value(transparent)]
548pub struct Entrypoints(FxIndexMap<AppPath, Entrypoint>);
549
550fn is_parallel_route(name: &str) -> bool {
551 name.starts_with('@')
552}
553
554fn is_group_route(name: &str) -> bool {
555 name.starts_with('(') && name.ends_with(')')
556}
557
558fn match_parallel_route(name: &str) -> Option<&str> {
559 name.strip_prefix('@')
560}
561
562fn conflict_issue(
563 app_dir: ResolvedVc<FileSystemPath>,
564 e: &'_ OccupiedEntry<'_, AppPath, Entrypoint>,
565 a: &str,
566 b: &str,
567 value_a: &AppPage,
568 value_b: &AppPage,
569) {
570 let item_names = if a == b {
571 format!("{a}s")
572 } else {
573 format!("{a} and {b}")
574 };
575
576 DirectoryTreeIssue {
577 app_dir,
578 message: StyledString::Text(
579 format!(
580 "Conflicting {} at {}: {a} at {value_a} and {b} at {value_b}",
581 item_names,
582 e.key(),
583 )
584 .into(),
585 )
586 .resolved_cell(),
587 severity: IssueSeverity::Error.resolved_cell(),
588 }
589 .resolved_cell()
590 .emit();
591}
592
593fn add_app_page(
594 app_dir: ResolvedVc<FileSystemPath>,
595 result: &mut FxIndexMap<AppPath, Entrypoint>,
596 page: AppPage,
597 loader_tree: ResolvedVc<AppPageLoaderTree>,
598) {
599 let mut e = match result.entry(page.clone().into()) {
600 Entry::Occupied(e) => e,
601 Entry::Vacant(e) => {
602 e.insert(Entrypoint::AppPage {
603 pages: vec![page],
604 loader_tree,
605 });
606 return;
607 }
608 };
609
610 let conflict = |existing_name: &str, existing_page: &AppPage| {
611 conflict_issue(app_dir, &e, "page", existing_name, &page, existing_page);
612 };
613
614 let value = e.get();
615 match value {
616 Entrypoint::AppPage {
617 pages: existing_pages,
618 loader_tree: existing_loader_tree,
619 } => {
620 if *existing_loader_tree != loader_tree {
623 conflict("page", existing_pages.first().unwrap());
624 }
625
626 let Entrypoint::AppPage {
627 pages: stored_pages,
628 ..
629 } = e.get_mut()
630 else {
631 unreachable!("Entrypoint::AppPage was already matched");
632 };
633
634 stored_pages.push(page);
635 stored_pages.sort();
636 }
637 Entrypoint::AppRoute {
638 page: existing_page,
639 ..
640 } => {
641 conflict("route", existing_page);
642 }
643 Entrypoint::AppMetadata {
644 page: existing_page,
645 ..
646 } => {
647 conflict("metadata", existing_page);
648 }
649 }
650}
651
652fn add_app_route(
653 app_dir: ResolvedVc<FileSystemPath>,
654 result: &mut FxIndexMap<AppPath, Entrypoint>,
655 page: AppPage,
656 path: ResolvedVc<FileSystemPath>,
657 root_layouts: ResolvedVc<FileSystemPathVec>,
658) {
659 let e = match result.entry(page.clone().into()) {
660 Entry::Occupied(e) => e,
661 Entry::Vacant(e) => {
662 e.insert(Entrypoint::AppRoute {
663 page,
664 path,
665 root_layouts,
666 });
667 return;
668 }
669 };
670
671 let conflict = |existing_name: &str, existing_page: &AppPage| {
672 conflict_issue(app_dir, &e, "route", existing_name, &page, existing_page);
673 };
674
675 let value = e.get();
676 match value {
677 Entrypoint::AppPage { pages, .. } => {
678 conflict("page", pages.first().unwrap());
679 }
680 Entrypoint::AppRoute {
681 page: existing_page,
682 ..
683 } => {
684 conflict("route", existing_page);
685 }
686 Entrypoint::AppMetadata {
687 page: existing_page,
688 ..
689 } => {
690 conflict("metadata", existing_page);
691 }
692 }
693}
694
695fn add_app_metadata_route(
696 app_dir: ResolvedVc<FileSystemPath>,
697 result: &mut FxIndexMap<AppPath, Entrypoint>,
698 page: AppPage,
699 metadata: MetadataItem,
700) {
701 let e = match result.entry(page.clone().into()) {
702 Entry::Occupied(e) => e,
703 Entry::Vacant(e) => {
704 e.insert(Entrypoint::AppMetadata { page, metadata });
705 return;
706 }
707 };
708
709 let conflict = |existing_name: &str, existing_page: &AppPage| {
710 conflict_issue(app_dir, &e, "metadata", existing_name, &page, existing_page);
711 };
712
713 let value = e.get();
714 match value {
715 Entrypoint::AppPage { pages, .. } => {
716 conflict("page", pages.first().unwrap());
717 }
718 Entrypoint::AppRoute {
719 page: existing_page,
720 ..
721 } => {
722 conflict("route", existing_page);
723 }
724 Entrypoint::AppMetadata {
725 page: existing_page,
726 ..
727 } => {
728 conflict("metadata", existing_page);
729 }
730 }
731}
732
733#[turbo_tasks::function]
734pub fn get_entrypoints(
735 app_dir: Vc<FileSystemPath>,
736 page_extensions: Vc<Vec<RcStr>>,
737 is_global_not_found_enabled: Vc<bool>,
738) -> Vc<Entrypoints> {
739 directory_tree_to_entrypoints(
740 app_dir,
741 get_directory_tree(app_dir, page_extensions),
742 get_global_metadata(app_dir, page_extensions),
743 is_global_not_found_enabled,
744 Default::default(),
745 )
746}
747
748#[turbo_tasks::function]
749fn directory_tree_to_entrypoints(
750 app_dir: Vc<FileSystemPath>,
751 directory_tree: Vc<DirectoryTree>,
752 global_metadata: Vc<GlobalMetadata>,
753 is_global_not_found_enabled: Vc<bool>,
754 root_layouts: Vc<FileSystemPathVec>,
755) -> Vc<Entrypoints> {
756 directory_tree_to_entrypoints_internal(
757 app_dir,
758 global_metadata,
759 is_global_not_found_enabled,
760 "".into(),
761 directory_tree,
762 AppPage::new(),
763 root_layouts,
764 )
765}
766
767#[turbo_tasks::value]
768struct DuplicateParallelRouteIssue {
769 app_dir: ResolvedVc<FileSystemPath>,
770 page: AppPage,
771}
772
773#[turbo_tasks::value_impl]
774impl Issue for DuplicateParallelRouteIssue {
775 #[turbo_tasks::function]
776 fn file_path(&self) -> Vc<FileSystemPath> {
777 self.app_dir.join(self.page.to_string().into())
778 }
779
780 #[turbo_tasks::function]
781 fn stage(self: Vc<Self>) -> Vc<IssueStage> {
782 IssueStage::ProcessModule.cell()
783 }
784
785 #[turbo_tasks::function]
786 fn title(self: Vc<Self>) -> Vc<StyledString> {
787 StyledString::Text(
788 "You cannot have two parallel pages that resolve to the same path.".into(),
789 )
790 .cell()
791 }
792}
793
794fn page_path_except_parallel(loader_tree: &AppPageLoaderTree) -> Option<AppPage> {
795 if loader_tree.page.iter().any(|v| {
796 matches!(
797 v,
798 PageSegment::CatchAll(..)
799 | PageSegment::OptionalCatchAll(..)
800 | PageSegment::Parallel(..)
801 )
802 }) {
803 return None;
804 }
805
806 if loader_tree.modules.page.is_some() {
807 return Some(loader_tree.page.clone());
808 }
809
810 if let Some(children) = loader_tree.parallel_routes.get("children") {
811 return page_path_except_parallel(children);
812 }
813
814 None
815}
816
817async fn check_duplicate(
818 duplicate: &mut FxHashMap<AppPath, AppPage>,
819 loader_tree: &AppPageLoaderTree,
820 app_dir: Vc<FileSystemPath>,
821) -> Result<()> {
822 let page_path = page_path_except_parallel(loader_tree);
823
824 if let Some(page_path) = page_path {
825 if let Some(prev) = duplicate.insert(AppPath::from(page_path.clone()), page_path.clone()) {
826 if prev != page_path {
827 DuplicateParallelRouteIssue {
828 app_dir: app_dir.to_resolved().await?,
829 page: loader_tree.page.clone(),
830 }
831 .resolved_cell()
832 .emit();
833 }
834 }
835 }
836
837 Ok(())
838}
839
840#[turbo_tasks::value(transparent)]
841struct AppPageLoaderTreeOption(Option<ResolvedVc<AppPageLoaderTree>>);
842
843#[turbo_tasks::function]
845async fn directory_tree_to_loader_tree(
846 app_dir: Vc<FileSystemPath>,
847 global_metadata: Vc<GlobalMetadata>,
848 directory_name: RcStr,
849 directory_tree: Vc<DirectoryTree>,
850 app_page: AppPage,
851 for_app_path: AppPath,
853) -> Result<Vc<AppPageLoaderTreeOption>> {
854 let plain_tree = &*directory_tree.into_plain().await?;
855
856 let tree = directory_tree_to_loader_tree_internal(
857 app_dir,
858 global_metadata,
859 directory_name,
860 plain_tree,
861 app_page,
862 for_app_path,
863 )
864 .await?;
865
866 Ok(Vc::cell(tree.map(AppPageLoaderTree::resolved_cell)))
867}
868
869async fn directory_tree_to_loader_tree_internal(
870 app_dir: Vc<FileSystemPath>,
871 global_metadata: Vc<GlobalMetadata>,
872 directory_name: RcStr,
873 directory_tree: &PlainDirectoryTree,
874 app_page: AppPage,
875 for_app_path: AppPath,
877) -> Result<Option<AppPageLoaderTree>> {
878 let app_path = AppPath::from(app_page.clone());
879
880 if !for_app_path.contains(&app_path) {
881 return Ok(None);
882 }
883
884 let mut modules = directory_tree.modules.clone();
885
886 modules.metadata.base_page = Some(app_page.clone());
889
890 let is_root_directory = app_page.is_root();
892 let is_root_layout = app_path.is_root() && modules.layout.is_some();
895
896 if is_root_directory || is_root_layout {
897 if modules.not_found.is_none() {
898 modules.not_found = Some(
899 get_next_package(app_dir)
900 .join("dist/client/components/not-found-error.js".into())
901 .to_resolved()
902 .await?,
903 );
904 }
905 if modules.forbidden.is_none() {
906 modules.forbidden = Some(
907 get_next_package(app_dir)
908 .join("dist/client/components/forbidden-error.js".into())
909 .to_resolved()
910 .await?,
911 );
912 }
913 if modules.unauthorized.is_none() {
914 modules.unauthorized = Some(
915 get_next_package(app_dir)
916 .join("dist/client/components/unauthorized-error.js".into())
917 .to_resolved()
918 .await?,
919 );
920 }
921 }
922
923 let mut tree = AppPageLoaderTree {
924 page: app_page.clone(),
925 segment: directory_name.clone(),
926 parallel_routes: FxIndexMap::default(),
927 modules: modules.without_leafs(),
928 global_metadata: global_metadata.to_resolved().await?,
929 };
930
931 let current_level_is_parallel_route = is_parallel_route(&directory_name);
932
933 if current_level_is_parallel_route {
934 tree.segment = "children".into();
935 }
936
937 if let Some(page) = (app_path == for_app_path || app_path.is_catchall())
938 .then_some(modules.page)
939 .flatten()
940 {
941 tree.parallel_routes.insert(
942 "children".into(),
943 AppPageLoaderTree {
944 page: app_page.clone(),
945 segment: "__PAGE__".into(),
946 parallel_routes: FxIndexMap::default(),
947 modules: AppDirModules {
948 page: Some(page),
949 metadata: modules.metadata,
950 ..Default::default()
951 },
952 global_metadata: global_metadata.to_resolved().await?,
953 },
954 );
955
956 if current_level_is_parallel_route {
957 tree.segment = "page$".into();
958 }
959 }
960
961 let mut duplicate = FxHashMap::default();
962
963 for (subdir_name, subdirectory) in &directory_tree.subdirectories {
964 let parallel_route_key = match_parallel_route(subdir_name);
965
966 let mut child_app_page = app_page.clone();
967 let mut illegal_path_error = None;
968
969 if let Err(e) = child_app_page.push_str(subdir_name) {
973 illegal_path_error = Some(e);
974 }
975
976 let subtree = Box::pin(directory_tree_to_loader_tree_internal(
977 app_dir,
978 global_metadata,
979 subdir_name.clone(),
980 subdirectory,
981 child_app_page.clone(),
982 for_app_path.clone(),
983 ))
984 .await?;
985
986 if let Some(illegal_path) = subtree.as_ref().and(illegal_path_error) {
987 return Err(illegal_path);
988 }
989
990 if let Some(subtree) = subtree {
991 if let Some(key) = parallel_route_key {
992 tree.parallel_routes.insert(key.into(), subtree);
993 continue;
994 }
995
996 if is_group_route(subdir_name) && !subtree.has_page() {
998 continue;
999 }
1000
1001 if subtree.has_page() {
1002 check_duplicate(&mut duplicate, &subtree, app_dir).await?;
1003 }
1004
1005 if let Some(current_tree) = tree.parallel_routes.get("children") {
1006 if current_tree.has_only_catchall()
1007 && (!subtree.has_only_catchall()
1008 || current_tree.get_specificity() < subtree.get_specificity())
1009 {
1010 tree.parallel_routes
1011 .insert("children".into(), subtree.clone());
1012 }
1013 } else {
1014 tree.parallel_routes.insert("children".into(), subtree);
1015 }
1016 } else if let Some(key) = parallel_route_key {
1017 bail!(
1018 "missing page or default for parallel route `{}` (page: {})",
1019 key,
1020 app_page
1021 );
1022 }
1023 }
1024
1025 if tree
1028 .parallel_routes
1029 .iter()
1030 .any(|(_, parallel_tree)| parallel_tree.is_intercepting())
1031 {
1032 let mut keys_to_replace = Vec::new();
1033
1034 for (key, parallel_tree) in &tree.parallel_routes {
1035 if !parallel_tree.is_intercepting() {
1036 keys_to_replace.push(key.clone());
1037 }
1038 }
1039
1040 for key in keys_to_replace {
1041 let subdir_name: RcStr = format!("@{key}").into();
1042
1043 let default = if key == "children" {
1044 modules.default
1045 } else if let Some(subdirectory) = directory_tree.subdirectories.get(&subdir_name) {
1046 subdirectory.modules.default
1047 } else {
1048 None
1049 };
1050
1051 tree.parallel_routes.insert(
1052 key,
1053 default_route_tree(
1054 app_dir,
1055 global_metadata,
1056 app_page.clone(),
1057 default.map(|v| *v),
1058 )
1059 .await?,
1060 );
1061 }
1062 }
1063
1064 if tree.parallel_routes.is_empty() {
1065 if modules.default.is_some() || current_level_is_parallel_route {
1066 tree = default_route_tree(
1067 app_dir,
1068 global_metadata,
1069 app_page,
1070 modules.default.map(|v| *v),
1071 )
1072 .await?;
1073 } else {
1074 return Ok(None);
1075 }
1076 } else if tree.parallel_routes.get("children").is_none() {
1077 tree.parallel_routes.insert(
1078 "children".into(),
1079 default_route_tree(
1080 app_dir,
1081 global_metadata,
1082 app_page,
1083 modules.default.map(|v| *v),
1084 )
1085 .await?,
1086 );
1087 }
1088
1089 if tree.parallel_routes.len() > 1
1090 && tree.parallel_routes.keys().next().map(|s| s.as_str()) != Some("children")
1091 {
1092 tree.parallel_routes
1094 .move_index(tree.parallel_routes.len() - 1, 0);
1095 }
1096
1097 Ok(Some(tree))
1098}
1099
1100async fn default_route_tree(
1101 app_dir: Vc<FileSystemPath>,
1102 global_metadata: Vc<GlobalMetadata>,
1103 app_page: AppPage,
1104 default_component: Option<Vc<FileSystemPath>>,
1105) -> Result<AppPageLoaderTree> {
1106 Ok(AppPageLoaderTree {
1107 page: app_page.clone(),
1108 segment: "__DEFAULT__".into(),
1109 parallel_routes: FxIndexMap::default(),
1110 modules: if let Some(default) = default_component {
1111 AppDirModules {
1112 default: Some(default.to_resolved().await?),
1113 ..Default::default()
1114 }
1115 } else {
1116 AppDirModules {
1118 default: Some(
1119 get_next_package(app_dir)
1120 .join("dist/client/components/parallel-route-default.js".into())
1121 .to_resolved()
1122 .await?,
1123 ),
1124 ..Default::default()
1125 }
1126 },
1127 global_metadata: global_metadata.to_resolved().await?,
1128 })
1129}
1130
1131#[turbo_tasks::function]
1132async fn directory_tree_to_entrypoints_internal(
1133 app_dir: ResolvedVc<FileSystemPath>,
1134 global_metadata: Vc<GlobalMetadata>,
1135 is_global_not_found_enabled: Vc<bool>,
1136 directory_name: RcStr,
1137 directory_tree: Vc<DirectoryTree>,
1138 app_page: AppPage,
1139 root_layouts: ResolvedVc<FileSystemPathVec>,
1140) -> Result<Vc<Entrypoints>> {
1141 let span = tracing::info_span!("build layout trees", name = display(&app_page));
1142 directory_tree_to_entrypoints_internal_untraced(
1143 app_dir,
1144 global_metadata,
1145 is_global_not_found_enabled,
1146 directory_name,
1147 directory_tree,
1148 app_page,
1149 root_layouts,
1150 )
1151 .instrument(span)
1152 .await
1153}
1154
1155async fn directory_tree_to_entrypoints_internal_untraced(
1156 app_dir: ResolvedVc<FileSystemPath>,
1157 global_metadata: Vc<GlobalMetadata>,
1158 is_global_not_found_enabled: Vc<bool>,
1159 directory_name: RcStr,
1160 directory_tree: Vc<DirectoryTree>,
1161 app_page: AppPage,
1162 root_layouts: ResolvedVc<FileSystemPathVec>,
1163) -> Result<Vc<Entrypoints>> {
1164 let mut result = FxIndexMap::default();
1165
1166 let directory_tree_vc = directory_tree;
1167 let directory_tree = &*directory_tree.await?;
1168
1169 let subdirectories = &directory_tree.subdirectories;
1170 let modules = &directory_tree.modules;
1171 let root_layouts = if let Some(layout) = modules.layout {
1175 let mut layouts = root_layouts.owned().await?;
1176 layouts.push(layout);
1177 ResolvedVc::cell(layouts)
1178 } else {
1179 root_layouts
1180 };
1181
1182 if modules.page.is_some() {
1183 let app_path = AppPath::from(app_page.clone());
1184
1185 let loader_tree = *directory_tree_to_loader_tree(
1186 *app_dir,
1187 global_metadata,
1188 directory_name.clone(),
1189 directory_tree_vc,
1190 app_page.clone(),
1191 app_path,
1192 )
1193 .await?;
1194
1195 add_app_page(
1196 app_dir,
1197 &mut result,
1198 app_page.complete(PageType::Page)?,
1199 loader_tree.context("loader tree should be created for a page/default")?,
1200 );
1201 }
1202
1203 if let Some(route) = modules.route {
1204 add_app_route(
1205 app_dir,
1206 &mut result,
1207 app_page.complete(PageType::Route)?,
1208 route,
1209 root_layouts,
1210 );
1211 }
1212
1213 let Metadata {
1214 icon,
1215 apple,
1216 twitter,
1217 open_graph,
1218 sitemap,
1219 base_page: _,
1220 } = &modules.metadata;
1221
1222 for meta in sitemap
1223 .iter()
1224 .copied()
1225 .chain(icon.iter().copied().map(MetadataItem::from))
1226 .chain(apple.iter().copied().map(MetadataItem::from))
1227 .chain(twitter.iter().copied().map(MetadataItem::from))
1228 .chain(open_graph.iter().copied().map(MetadataItem::from))
1229 {
1230 let app_page = app_page.clone_push_str(&get_metadata_route_name(meta).await?)?;
1231
1232 add_app_metadata_route(
1233 app_dir,
1234 &mut result,
1235 normalize_metadata_route(app_page)?,
1236 meta,
1237 );
1238 }
1239
1240 if app_page.is_root() {
1242 let GlobalMetadata {
1243 favicon,
1244 robots,
1245 manifest,
1246 } = &*global_metadata.await?;
1247
1248 for meta in favicon.iter().chain(robots.iter()).chain(manifest.iter()) {
1249 let app_page = app_page.clone_push_str(&get_metadata_route_name(*meta).await?)?;
1250
1251 add_app_metadata_route(
1252 app_dir,
1253 &mut result,
1254 normalize_metadata_route(app_page)?,
1255 *meta,
1256 );
1257 }
1258
1259 let mut modules = directory_tree.modules.clone();
1260
1261 if modules.layout.is_none() {
1263 modules.layout = Some(
1264 get_next_package(*app_dir)
1265 .join("dist/client/components/default-layout.js".into())
1266 .to_resolved()
1267 .await?,
1268 );
1269 }
1270
1271 if modules.not_found.is_none() {
1272 modules.not_found = Some(
1273 get_next_package(*app_dir)
1274 .join("dist/client/components/not-found-error.js".into())
1275 .to_resolved()
1276 .await?,
1277 );
1278 }
1279 if modules.forbidden.is_none() {
1280 modules.forbidden = Some(
1281 get_next_package(*app_dir)
1282 .join("dist/client/components/forbidden-error.js".into())
1283 .to_resolved()
1284 .await?,
1285 );
1286 }
1287 if modules.unauthorized.is_none() {
1288 modules.unauthorized = Some(
1289 get_next_package(*app_dir)
1290 .join("dist/client/components/unauthorized-error.js".into())
1291 .to_resolved()
1292 .await?,
1293 );
1294 }
1295
1296 let is_global_not_found_enabled = *is_global_not_found_enabled.await?;
1301 let use_global_not_found =
1302 is_global_not_found_enabled || modules.global_not_found.is_some();
1303
1304 let not_found_root_modules = modules.without_leafs();
1305 let not_found_tree = AppPageLoaderTree {
1306 page: app_page.clone(),
1307 segment: directory_name.clone(),
1308 parallel_routes: fxindexmap! {
1309 "children".into() => AppPageLoaderTree {
1310 page: app_page.clone(),
1311 segment: "/_not-found".into(),
1312 parallel_routes: fxindexmap! {
1313 "children".into() => AppPageLoaderTree {
1314 page: app_page.clone(),
1315 segment: "__PAGE__".into(),
1316 parallel_routes: FxIndexMap::default(),
1317 modules: if use_global_not_found {
1318 AppDirModules {
1321 layout: None,
1322 page: match modules.global_not_found {
1323 Some(v) => Some(v),
1324 None => Some(get_next_package(*app_dir)
1325 .join("dist/client/components/global-not-found.js".into())
1326 .to_resolved()
1327 .await?),
1328 },
1329 ..Default::default()
1330 }
1331 } else {
1332 AppDirModules {
1335 page: match modules.not_found {
1336 Some(v) => Some(v),
1337 None => Some(get_next_package(*app_dir)
1338 .join("dist/client/components/not-found-error.js".into())
1339 .to_resolved()
1340 .await?),
1341 },
1342 ..Default::default()
1343 }
1344 },
1345 global_metadata: global_metadata.to_resolved().await?,
1346 }
1347 },
1348 modules: AppDirModules {
1349 ..Default::default()
1350 },
1351 global_metadata: global_metadata.to_resolved().await?,
1352 },
1353 },
1354 modules: AppDirModules {
1355 layout: if use_global_not_found {
1359 None
1360 } else {
1361 modules.layout
1362 },
1363 ..not_found_root_modules
1364 },
1365 global_metadata: global_metadata.to_resolved().await?,
1366 }
1367 .resolved_cell();
1368
1369 {
1370 let app_page = app_page
1371 .clone_push_str("_not-found")?
1372 .complete(PageType::Page)?;
1373
1374 add_app_page(app_dir, &mut result, app_page, not_found_tree);
1375 }
1376 }
1377
1378 let app_page = &app_page;
1379 let directory_name = &directory_name;
1380 let subdirectories = subdirectories
1381 .iter()
1382 .map(|(subdir_name, &subdirectory)| async move {
1383 let mut child_app_page = app_page.clone();
1384 let mut illegal_path = None;
1385
1386 if let Err(e) = child_app_page.push_str(subdir_name) {
1390 illegal_path = Some(e);
1391 }
1392
1393 let map = directory_tree_to_entrypoints_internal(
1394 *app_dir,
1395 global_metadata,
1396 is_global_not_found_enabled,
1397 subdir_name.clone(),
1398 *subdirectory,
1399 child_app_page.clone(),
1400 *root_layouts,
1401 )
1402 .await?;
1403
1404 if let Some(illegal_path) = illegal_path {
1405 if !map.is_empty() {
1406 return Err(illegal_path);
1407 }
1408 }
1409
1410 let mut loader_trees = Vec::new();
1411
1412 for (_, entrypoint) in map.iter() {
1413 if let Entrypoint::AppPage {
1414 ref pages,
1415 loader_tree: _,
1416 } = *entrypoint
1417 {
1418 for page in pages {
1419 let app_path = AppPath::from(page.clone());
1420
1421 let loader_tree = directory_tree_to_loader_tree(
1422 *app_dir,
1423 global_metadata,
1424 directory_name.clone(),
1425 directory_tree_vc,
1426 app_page.clone(),
1427 app_path,
1428 );
1429 loader_trees.push(loader_tree);
1430 }
1431 }
1432 }
1433 Ok((map, loader_trees))
1434 })
1435 .try_join()
1436 .await?;
1437
1438 for (map, loader_trees) in subdirectories.iter() {
1439 let mut i = 0;
1440 for (_, entrypoint) in map.iter() {
1441 match *entrypoint {
1442 Entrypoint::AppPage {
1443 ref pages,
1444 loader_tree: _,
1445 } => {
1446 for page in pages {
1447 let loader_tree = *loader_trees[i].await?;
1448 i += 1;
1449
1450 add_app_page(
1451 app_dir,
1452 &mut result,
1453 page.clone(),
1454 loader_tree
1455 .context("loader tree should be created for a page/default")?,
1456 );
1457 }
1458 }
1459 Entrypoint::AppRoute {
1460 ref page,
1461 path,
1462 root_layouts,
1463 } => {
1464 add_app_route(app_dir, &mut result, page.clone(), path, root_layouts);
1465 }
1466 Entrypoint::AppMetadata { ref page, metadata } => {
1467 add_app_metadata_route(app_dir, &mut result, page.clone(), metadata);
1468 }
1469 }
1470 }
1471 }
1472 Ok(Vc::cell(result))
1473}
1474
1475#[turbo_tasks::function]
1477pub async fn get_global_metadata(
1478 app_dir: Vc<FileSystemPath>,
1479 page_extensions: Vc<Vec<RcStr>>,
1480) -> Result<Vc<GlobalMetadata>> {
1481 let DirectoryContent::Entries(entries) = &*app_dir.read_dir().await? else {
1482 bail!("app_dir must be a directory")
1483 };
1484 let mut metadata = GlobalMetadata::default();
1485
1486 for (basename, entry) in entries {
1487 let DirectoryEntry::File(file) = *entry else {
1488 continue;
1489 };
1490
1491 let Some(GlobalMetadataFileMatch {
1492 metadata_type,
1493 dynamic,
1494 }) = match_global_metadata_file(basename, &page_extensions.await?)
1495 else {
1496 continue;
1497 };
1498
1499 let entry = match metadata_type {
1500 "favicon" => &mut metadata.favicon,
1501 "manifest" => &mut metadata.manifest,
1502 "robots" => &mut metadata.robots,
1503 _ => continue,
1504 };
1505
1506 if dynamic {
1507 *entry = Some(MetadataItem::Dynamic { path: file });
1508 } else {
1509 *entry = Some(MetadataItem::Static { path: file });
1510 }
1511 }
1513
1514 Ok(metadata.cell())
1515}
1516
1517#[turbo_tasks::value(shared)]
1518struct DirectoryTreeIssue {
1519 pub severity: ResolvedVc<IssueSeverity>,
1520 pub app_dir: ResolvedVc<FileSystemPath>,
1521 pub message: ResolvedVc<StyledString>,
1522}
1523
1524#[turbo_tasks::value_impl]
1525impl Issue for DirectoryTreeIssue {
1526 #[turbo_tasks::function]
1527 fn severity(&self) -> Vc<IssueSeverity> {
1528 *self.severity
1529 }
1530
1531 #[turbo_tasks::function]
1532 fn title(&self) -> Vc<StyledString> {
1533 StyledString::Text("An issue occurred while preparing your Next.js app".into()).cell()
1534 }
1535
1536 #[turbo_tasks::function]
1537 fn stage(&self) -> Vc<IssueStage> {
1538 IssueStage::AppStructure.cell()
1539 }
1540
1541 #[turbo_tasks::function]
1542 fn file_path(&self) -> Vc<FileSystemPath> {
1543 *self.app_dir
1544 }
1545
1546 #[turbo_tasks::function]
1547 fn description(&self) -> Vc<OptionStyledString> {
1548 Vc::cell(Some(self.message))
1549 }
1550}