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}
205
206#[turbo_tasks::value_impl]
207impl DirectoryTree {
208 #[turbo_tasks::function]
209 pub async fn into_plain(&self) -> Result<Vc<PlainDirectoryTree>> {
210 let mut subdirectories = BTreeMap::new();
211
212 for (name, subdirectory) in &self.subdirectories {
213 subdirectories.insert(name.clone(), subdirectory.into_plain().owned().await?);
214 }
215
216 Ok(PlainDirectoryTree {
217 subdirectories,
218 modules: self.modules.clone(),
219 }
220 .cell())
221 }
222}
223
224#[turbo_tasks::value(transparent)]
225pub struct OptionAppDir(Option<FileSystemPath>);
226
227#[turbo_tasks::function]
229pub async fn find_app_dir(project_path: FileSystemPath) -> Result<Vc<OptionAppDir>> {
230 let app = project_path.join("app")?;
231 let src_app = project_path.join("src/app")?;
232 let app_dir = if *app.get_type().await? == FileSystemEntryType::Directory {
233 app
234 } else if *src_app.get_type().await? == FileSystemEntryType::Directory {
235 src_app
236 } else {
237 return Ok(Vc::cell(None));
238 };
239
240 Ok(Vc::cell(Some(app_dir)))
241}
242
243#[turbo_tasks::function]
244async fn get_directory_tree(
245 dir: FileSystemPath,
246 page_extensions: Vc<Vec<RcStr>>,
247) -> Result<Vc<DirectoryTree>> {
248 let span = tracing::info_span!(
249 "read app directory tree",
250 name = display(dir.value_to_string().await?)
251 );
252 get_directory_tree_internal(dir, page_extensions)
253 .instrument(span)
254 .await
255}
256
257async fn get_directory_tree_internal(
258 dir: FileSystemPath,
259 page_extensions: Vc<Vec<RcStr>>,
260) -> Result<Vc<DirectoryTree>> {
261 let DirectoryContent::Entries(entries) = &*dir.read_dir().await? else {
262 return Ok(DirectoryTree {
266 subdirectories: Default::default(),
267 modules: AppDirModules::default(),
268 }
269 .cell());
270 };
271 let page_extensions_value = page_extensions.await?;
272
273 let mut subdirectories = BTreeMap::new();
274 let mut modules = AppDirModules::default();
275
276 let mut metadata_icon = Vec::new();
277 let mut metadata_apple = Vec::new();
278 let mut metadata_open_graph = Vec::new();
279 let mut metadata_twitter = Vec::new();
280
281 for (basename, entry) in entries {
282 let entry = entry.clone().resolve_symlink().await?;
283 match entry {
284 DirectoryEntry::File(file) => {
285 if basename.ends_with(".d.ts") {
287 continue;
288 }
289 if let Some((stem, ext)) = basename.split_once('.')
290 && page_extensions_value.iter().any(|e| e == ext)
291 {
292 match stem {
293 "page" => modules.page = Some(file.clone()),
294 "layout" => modules.layout = Some(file.clone()),
295 "error" => modules.error = Some(file.clone()),
296 "global-error" => modules.global_error = Some(file.clone()),
297 "global-not-found" => modules.global_not_found = Some(file.clone()),
298 "loading" => modules.loading = Some(file.clone()),
299 "template" => modules.template = Some(file.clone()),
300 "forbidden" => modules.forbidden = Some(file.clone()),
301 "unauthorized" => modules.unauthorized = Some(file.clone()),
302 "not-found" => modules.not_found = Some(file.clone()),
303 "default" => modules.default = Some(file.clone()),
304 "route" => modules.route = Some(file.clone()),
305 _ => {}
306 }
307 }
308
309 let Some(MetadataFileMatch {
310 metadata_type,
311 number,
312 dynamic,
313 }) = match_local_metadata_file(basename.as_str(), &page_extensions_value)
314 else {
315 continue;
316 };
317
318 let entry = match metadata_type {
319 "icon" => &mut metadata_icon,
320 "apple-icon" => &mut metadata_apple,
321 "twitter-image" => &mut metadata_twitter,
322 "opengraph-image" => &mut metadata_open_graph,
323 "sitemap" => {
324 if dynamic {
325 modules.metadata.sitemap = Some(MetadataItem::Dynamic { path: file });
326 } else {
327 modules.metadata.sitemap = Some(MetadataItem::Static { path: file });
328 }
329 continue;
330 }
331 _ => continue,
332 };
333
334 if dynamic {
335 entry.push((number, MetadataWithAltItem::Dynamic { path: file }));
336 continue;
337 }
338
339 let file_name = file.file_name();
340 let basename = file_name
341 .rsplit_once('.')
342 .map_or(file_name, |(basename, _)| basename);
343 let alt_path = file.parent().join(&format!("{basename}.alt.txt"))?;
344 let alt_path = matches!(&*alt_path.get_type().await?, FileSystemEntryType::File)
345 .then_some(alt_path);
346
347 entry.push((
348 number,
349 MetadataWithAltItem::Static {
350 path: file,
351 alt_path,
352 },
353 ));
354 }
355 DirectoryEntry::Directory(dir) => {
356 if !basename.starts_with('_') {
358 let result = get_directory_tree(dir.clone(), page_extensions)
359 .to_resolved()
360 .await?;
361 subdirectories.insert(basename.clone(), result);
362 }
363 }
364 _ => {}
366 }
367 }
368
369 fn sort<T>(mut list: Vec<(Option<u32>, T)>) -> Vec<T> {
370 list.sort_by_key(|(num, _)| *num);
371 list.into_iter().map(|(_, item)| item).collect()
372 }
373
374 modules.metadata.icon = sort(metadata_icon);
375 modules.metadata.apple = sort(metadata_apple);
376 modules.metadata.twitter = sort(metadata_twitter);
377 modules.metadata.open_graph = sort(metadata_open_graph);
378
379 Ok(DirectoryTree {
380 subdirectories,
381 modules,
382 }
383 .cell())
384}
385
386#[turbo_tasks::value]
387#[derive(Debug, Clone)]
388pub struct AppPageLoaderTree {
389 pub page: AppPage,
390 pub segment: RcStr,
391 #[bincode(with = "turbo_bincode::indexmap")]
392 pub parallel_routes: FxIndexMap<RcStr, AppPageLoaderTree>,
393 pub modules: AppDirModules,
394 pub global_metadata: ResolvedVc<GlobalMetadata>,
395}
396
397impl AppPageLoaderTree {
398 pub fn has_page(&self) -> bool {
400 if &*self.segment == "__PAGE__" {
401 return true;
402 }
403
404 for (_, tree) in &self.parallel_routes {
405 if tree.has_page() {
406 return true;
407 }
408 }
409
410 false
411 }
412
413 pub fn has_only_catchall(&self) -> bool {
416 if &*self.segment == "__PAGE__" && !self.page.is_catchall() {
417 return false;
418 }
419
420 for (_, tree) in &self.parallel_routes {
421 if !tree.has_only_catchall() {
422 return false;
423 }
424 }
425
426 true
427 }
428
429 pub fn is_intercepting(&self) -> bool {
431 if self.page.is_intercepting() && self.has_page() {
432 return true;
433 }
434
435 for (_, tree) in &self.parallel_routes {
436 if tree.is_intercepting() {
437 return true;
438 }
439 }
440
441 false
442 }
443
444 pub fn get_specificity(&self) -> usize {
447 if &*self.segment == "__PAGE__" {
448 return AppPath::from(self.page.clone()).len();
449 }
450
451 let mut specificity = 0;
452
453 for (_, tree) in &self.parallel_routes {
454 specificity = specificity.max(tree.get_specificity());
455 }
456
457 specificity
458 }
459}
460
461#[turbo_tasks::value(transparent)]
462#[derive(Default)]
463pub struct RootParamVecOption(Option<Vec<RcStr>>);
464
465#[turbo_tasks::value_impl]
466impl ValueDefault for RootParamVecOption {
467 #[turbo_tasks::function]
468 fn value_default() -> Vc<Self> {
469 Vc::cell(Default::default())
470 }
471}
472
473#[turbo_tasks::value(transparent)]
474pub struct FileSystemPathVec(Vec<FileSystemPath>);
475
476#[turbo_tasks::value_impl]
477impl ValueDefault for FileSystemPathVec {
478 #[turbo_tasks::function]
479 fn value_default() -> Vc<Self> {
480 Vc::cell(Vec::new())
481 }
482}
483
484#[derive(
485 Clone,
486 PartialEq,
487 Eq,
488 Hash,
489 TraceRawVcs,
490 ValueDebugFormat,
491 Debug,
492 TaskInput,
493 NonLocalValue,
494 Encode,
495 Decode,
496)]
497pub enum Entrypoint {
498 AppPage {
499 pages: Vec<AppPage>,
500 loader_tree: ResolvedVc<AppPageLoaderTree>,
501 root_params: ResolvedVc<RootParamVecOption>,
502 },
503 AppRoute {
504 page: AppPage,
505 path: FileSystemPath,
506 root_layouts: ResolvedVc<FileSystemPathVec>,
507 root_params: ResolvedVc<RootParamVecOption>,
508 },
509 AppMetadata {
510 page: AppPage,
511 metadata: MetadataItem,
512 root_params: ResolvedVc<RootParamVecOption>,
513 },
514}
515
516impl Entrypoint {
517 pub fn page(&self) -> &AppPage {
518 match self {
519 Entrypoint::AppPage { pages, .. } => pages.first().unwrap(),
520 Entrypoint::AppRoute { page, .. } => page,
521 Entrypoint::AppMetadata { page, .. } => page,
522 }
523 }
524 pub fn root_params(&self) -> ResolvedVc<RootParamVecOption> {
525 match self {
526 Entrypoint::AppPage { root_params, .. } => *root_params,
527 Entrypoint::AppRoute { root_params, .. } => *root_params,
528 Entrypoint::AppMetadata { root_params, .. } => *root_params,
529 }
530 }
531}
532
533#[turbo_tasks::value(transparent)]
534pub struct Entrypoints(
535 #[bincode(with = "turbo_bincode::indexmap")] FxIndexMap<AppPath, Entrypoint>,
536);
537
538fn is_parallel_route(name: &str) -> bool {
539 name.starts_with('@')
540}
541
542fn is_group_route(name: &str) -> bool {
543 name.starts_with('(') && name.ends_with(')')
544}
545
546fn match_parallel_route(name: &str) -> Option<&str> {
547 name.strip_prefix('@')
548}
549
550fn conflict_issue(
551 app_dir: FileSystemPath,
552 e: &'_ OccupiedEntry<'_, AppPath, Entrypoint>,
553 a: &str,
554 b: &str,
555 value_a: &AppPage,
556 value_b: &AppPage,
557) {
558 let item_names = if a == b {
559 format!("{a}s")
560 } else {
561 format!("{a} and {b}")
562 };
563
564 DirectoryTreeIssue {
565 app_dir,
566 message: StyledString::Text(
567 format!(
568 "Conflicting {} at {}: {a} at {value_a} and {b} at {value_b}",
569 item_names,
570 e.key(),
571 )
572 .into(),
573 )
574 .resolved_cell(),
575 severity: IssueSeverity::Error,
576 }
577 .resolved_cell()
578 .emit();
579}
580
581fn add_app_page(
582 app_dir: FileSystemPath,
583 result: &mut FxIndexMap<AppPath, Entrypoint>,
584 page: AppPage,
585 loader_tree: ResolvedVc<AppPageLoaderTree>,
586 root_params: ResolvedVc<RootParamVecOption>,
587) {
588 let mut e = match result.entry(page.clone().into()) {
589 Entry::Occupied(e) => e,
590 Entry::Vacant(e) => {
591 e.insert(Entrypoint::AppPage {
592 pages: vec![page],
593 loader_tree,
594 root_params,
595 });
596 return;
597 }
598 };
599
600 let conflict = |existing_name: &str, existing_page: &AppPage| {
601 conflict_issue(app_dir, &e, "page", existing_name, &page, existing_page);
602 };
603
604 let value = e.get();
605 match value {
606 Entrypoint::AppPage {
607 pages: existing_pages,
608 loader_tree: existing_loader_tree,
609 ..
610 } => {
611 if *existing_loader_tree != loader_tree {
614 conflict("page", existing_pages.first().unwrap());
615 }
616
617 let Entrypoint::AppPage {
618 pages: stored_pages,
619 ..
620 } = e.get_mut()
621 else {
622 unreachable!("Entrypoint::AppPage was already matched");
623 };
624
625 stored_pages.push(page);
626 stored_pages.sort();
627 }
628 Entrypoint::AppRoute {
629 page: existing_page,
630 ..
631 } => {
632 conflict("route", existing_page);
633 }
634 Entrypoint::AppMetadata {
635 page: existing_page,
636 ..
637 } => {
638 conflict("metadata", existing_page);
639 }
640 }
641}
642
643fn add_app_route(
644 app_dir: FileSystemPath,
645 result: &mut FxIndexMap<AppPath, Entrypoint>,
646 page: AppPage,
647 path: FileSystemPath,
648 root_layouts: ResolvedVc<FileSystemPathVec>,
649 root_params: ResolvedVc<RootParamVecOption>,
650) {
651 let e = match result.entry(page.clone().into()) {
652 Entry::Occupied(e) => e,
653 Entry::Vacant(e) => {
654 e.insert(Entrypoint::AppRoute {
655 page,
656 path,
657 root_layouts,
658 root_params,
659 });
660 return;
661 }
662 };
663
664 let conflict = |existing_name: &str, existing_page: &AppPage| {
665 conflict_issue(app_dir, &e, "route", existing_name, &page, existing_page);
666 };
667
668 let value = e.get();
669 match value {
670 Entrypoint::AppPage { pages, .. } => {
671 conflict("page", pages.first().unwrap());
672 }
673 Entrypoint::AppRoute {
674 page: existing_page,
675 ..
676 } => {
677 conflict("route", existing_page);
678 }
679 Entrypoint::AppMetadata {
680 page: existing_page,
681 ..
682 } => {
683 conflict("metadata", existing_page);
684 }
685 }
686}
687
688fn add_app_metadata_route(
689 app_dir: FileSystemPath,
690 result: &mut FxIndexMap<AppPath, Entrypoint>,
691 page: AppPage,
692 metadata: MetadataItem,
693 root_params: ResolvedVc<RootParamVecOption>,
694) {
695 let e = match result.entry(page.clone().into()) {
696 Entry::Occupied(e) => e,
697 Entry::Vacant(e) => {
698 e.insert(Entrypoint::AppMetadata {
699 page,
700 metadata,
701 root_params,
702 });
703 return;
704 }
705 };
706
707 let conflict = |existing_name: &str, existing_page: &AppPage| {
708 conflict_issue(app_dir, &e, "metadata", existing_name, &page, existing_page);
709 };
710
711 let value = e.get();
712 match value {
713 Entrypoint::AppPage { pages, .. } => {
714 conflict("page", pages.first().unwrap());
715 }
716 Entrypoint::AppRoute {
717 page: existing_page,
718 ..
719 } => {
720 conflict("route", existing_page);
721 }
722 Entrypoint::AppMetadata {
723 page: existing_page,
724 ..
725 } => {
726 conflict("metadata", existing_page);
727 }
728 }
729}
730
731#[turbo_tasks::function]
732pub fn get_entrypoints(
733 app_dir: FileSystemPath,
734 page_extensions: Vc<Vec<RcStr>>,
735 is_global_not_found_enabled: Vc<bool>,
736 next_mode: Vc<NextMode>,
737) -> Vc<Entrypoints> {
738 directory_tree_to_entrypoints(
739 app_dir.clone(),
740 get_directory_tree(app_dir.clone(), page_extensions),
741 get_global_metadata(app_dir, page_extensions),
742 is_global_not_found_enabled,
743 next_mode,
744 Default::default(),
745 Default::default(),
746 )
747}
748
749#[turbo_tasks::value(transparent)]
750pub struct CollectedRootParams(#[bincode(with = "turbo_bincode::indexset")] FxIndexSet<RcStr>);
751
752#[turbo_tasks::function]
753pub async fn collect_root_params(
754 entrypoints: ResolvedVc<Entrypoints>,
755) -> Result<Vc<CollectedRootParams>> {
756 let mut collected_root_params = FxIndexSet::<RcStr>::default();
757 for (_, entrypoint) in entrypoints.await?.iter() {
758 if let Some(ref root_params) = *entrypoint.root_params().await? {
759 collected_root_params.extend(root_params.iter().cloned());
760 }
761 }
762 Ok(Vc::cell(collected_root_params))
763}
764
765#[turbo_tasks::function]
766fn directory_tree_to_entrypoints(
767 app_dir: FileSystemPath,
768 directory_tree: Vc<DirectoryTree>,
769 global_metadata: Vc<GlobalMetadata>,
770 is_global_not_found_enabled: Vc<bool>,
771 next_mode: Vc<NextMode>,
772 root_layouts: Vc<FileSystemPathVec>,
773 root_params: Vc<RootParamVecOption>,
774) -> Vc<Entrypoints> {
775 directory_tree_to_entrypoints_internal(
776 app_dir,
777 global_metadata,
778 is_global_not_found_enabled,
779 next_mode,
780 rcstr!(""),
781 directory_tree,
782 AppPage::new(),
783 root_layouts,
784 root_params,
785 )
786}
787
788#[turbo_tasks::value]
789struct DuplicateParallelRouteIssue {
790 app_dir: FileSystemPath,
791 previously_inserted_page: AppPage,
792 page: AppPage,
793}
794
795#[turbo_tasks::value_impl]
796impl Issue for DuplicateParallelRouteIssue {
797 #[turbo_tasks::function]
798 fn file_path(&self) -> Result<Vc<FileSystemPath>> {
799 Ok(self.app_dir.join(&self.page.to_string())?.cell())
800 }
801
802 #[turbo_tasks::function]
803 fn stage(self: Vc<Self>) -> Vc<IssueStage> {
804 IssueStage::ProcessModule.cell()
805 }
806
807 #[turbo_tasks::function]
808 async fn title(self: Vc<Self>) -> Result<Vc<StyledString>> {
809 let this = self.await?;
810 Ok(StyledString::Text(
811 format!(
812 "You cannot have two parallel pages that resolve to the same path. Please check \
813 {} and {}.",
814 this.previously_inserted_page, this.page
815 )
816 .into(),
817 )
818 .cell())
819 }
820}
821
822#[turbo_tasks::value]
823struct MissingDefaultParallelRouteIssue {
824 app_dir: FileSystemPath,
825 app_page: AppPage,
826 slot_name: RcStr,
827}
828
829#[turbo_tasks::function]
830fn missing_default_parallel_route_issue(
831 app_dir: FileSystemPath,
832 app_page: AppPage,
833 slot_name: RcStr,
834) -> Vc<MissingDefaultParallelRouteIssue> {
835 MissingDefaultParallelRouteIssue {
836 app_dir,
837 app_page,
838 slot_name,
839 }
840 .cell()
841}
842
843#[turbo_tasks::value_impl]
844impl Issue for MissingDefaultParallelRouteIssue {
845 #[turbo_tasks::function]
846 fn file_path(&self) -> Result<Vc<FileSystemPath>> {
847 Ok(self
848 .app_dir
849 .join(&self.app_page.to_string())?
850 .join(&format!("@{}", self.slot_name))?
851 .cell())
852 }
853
854 #[turbo_tasks::function]
855 fn stage(self: Vc<Self>) -> Vc<IssueStage> {
856 IssueStage::AppStructure.cell()
857 }
858
859 fn severity(&self) -> IssueSeverity {
860 IssueSeverity::Error
861 }
862
863 #[turbo_tasks::function]
864 async fn title(&self) -> Vc<StyledString> {
865 StyledString::Text(
866 format!(
867 "Missing required default.js file for parallel route at {}/@{}",
868 self.app_page, self.slot_name
869 )
870 .into(),
871 )
872 .cell()
873 }
874
875 #[turbo_tasks::function]
876 async fn description(&self) -> Vc<OptionStyledString> {
877 Vc::cell(Some(
878 StyledString::Stack(vec![
879 StyledString::Text(
880 format!(
881 "The parallel route slot \"@{}\" is missing a default.js file. When using \
882 parallel routes, each slot must have a default.js file to serve as a \
883 fallback.",
884 self.slot_name
885 )
886 .into(),
887 ),
888 StyledString::Text(
889 format!(
890 "Create a default.js file at: {}/@{}/default.js",
891 self.app_page, self.slot_name
892 )
893 .into(),
894 ),
895 ])
896 .resolved_cell(),
897 ))
898 }
899
900 #[turbo_tasks::function]
901 fn documentation_link(&self) -> Vc<RcStr> {
902 Vc::cell(rcstr!(
903 "https://nextjs.org/docs/messages/slot-missing-default"
904 ))
905 }
906}
907
908fn page_path_except_parallel(loader_tree: &AppPageLoaderTree) -> Option<AppPage> {
909 if loader_tree.page.iter().any(|v| {
910 matches!(
911 v,
912 PageSegment::CatchAll(..)
913 | PageSegment::OptionalCatchAll(..)
914 | PageSegment::Parallel(..)
915 )
916 }) {
917 return None;
918 }
919
920 if loader_tree.modules.page.is_some() {
921 return Some(loader_tree.page.clone());
922 }
923
924 if let Some(children) = loader_tree.parallel_routes.get("children") {
925 return page_path_except_parallel(children);
926 }
927
928 None
929}
930
931fn has_child_routes(directory_tree: &PlainDirectoryTree) -> bool {
935 for (name, subdirectory) in &directory_tree.subdirectories {
936 if is_parallel_route(name) {
938 continue;
939 }
940
941 if is_group_route(name) {
943 if has_child_routes(subdirectory) {
945 return true;
946 }
947 continue;
948 }
949
950 return true;
952 }
953
954 false
955}
956
957async fn check_duplicate(
958 duplicate: &mut FxHashMap<AppPath, AppPage>,
959 loader_tree: &AppPageLoaderTree,
960 app_dir: FileSystemPath,
961) -> Result<()> {
962 let page_path = page_path_except_parallel(loader_tree);
963
964 if let Some(page_path) = page_path
965 && let Some(prev) = duplicate.insert(AppPath::from(page_path.clone()), page_path.clone())
966 && prev != page_path
967 {
968 DuplicateParallelRouteIssue {
969 app_dir: app_dir.clone(),
970 previously_inserted_page: prev.clone(),
971 page: loader_tree.page.clone(),
972 }
973 .resolved_cell()
974 .emit();
975 }
976
977 Ok(())
978}
979
980#[turbo_tasks::value(transparent)]
981struct AppPageLoaderTreeOption(Option<ResolvedVc<AppPageLoaderTree>>);
982
983#[turbo_tasks::function]
985async fn directory_tree_to_loader_tree(
986 app_dir: FileSystemPath,
987 global_metadata: Vc<GlobalMetadata>,
988 directory_name: RcStr,
989 directory_tree: Vc<DirectoryTree>,
990 app_page: AppPage,
991 for_app_path: AppPath,
993) -> Result<Vc<AppPageLoaderTreeOption>> {
994 let plain_tree = &*directory_tree.into_plain().await?;
995
996 let tree = directory_tree_to_loader_tree_internal(
997 app_dir,
998 global_metadata,
999 directory_name,
1000 plain_tree,
1001 app_page,
1002 for_app_path,
1003 AppDirModules::default(),
1004 )
1005 .await?;
1006
1007 Ok(Vc::cell(tree.map(AppPageLoaderTree::resolved_cell)))
1008}
1009
1010async fn check_and_update_module_references(
1025 app_dir: FileSystemPath,
1026 module: &mut Option<FileSystemPath>,
1027 parent_module: &mut Option<FileSystemPath>,
1028 file_path: &str,
1029 is_first_layer_group_route: bool,
1030) -> Result<()> {
1031 match (module.as_mut(), parent_module.as_mut()) {
1032 (Some(module), _) => *parent_module = Some(module.clone()),
1034 (None, Some(parent_module)) if is_first_layer_group_route => {
1037 *module = Some(parent_module.clone())
1038 }
1039 (None, Some(_)) => {}
1042 (None, None) => {
1046 let default_page = get_next_package(app_dir).await?.join(file_path)?;
1047 *module = Some(default_page.clone());
1048 *parent_module = Some(default_page);
1049 }
1050 }
1051
1052 Ok(())
1053}
1054
1055async fn check_and_update_global_module_references(
1063 app_dir: FileSystemPath,
1064 module: &mut Option<FileSystemPath>,
1065 file_path: &str,
1066) -> Result<()> {
1067 if module.is_none() {
1068 *module = Some(get_next_package(app_dir).await?.join(file_path)?);
1069 }
1070
1071 Ok(())
1072}
1073
1074async fn directory_tree_to_loader_tree_internal(
1075 app_dir: FileSystemPath,
1076 global_metadata: Vc<GlobalMetadata>,
1077 directory_name: RcStr,
1078 directory_tree: &PlainDirectoryTree,
1079 app_page: AppPage,
1080 for_app_path: AppPath,
1082 mut parent_modules: AppDirModules,
1083) -> Result<Option<AppPageLoaderTree>> {
1084 let app_path = AppPath::from(app_page.clone());
1085
1086 if !for_app_path.contains(&app_path) {
1087 return Ok(None);
1088 }
1089
1090 let mut modules = directory_tree.modules.clone();
1091
1092 modules.metadata.base_page = Some(app_page.clone());
1095
1096 let is_root_directory = app_page.is_root();
1098
1099 let is_first_layer_group_route = app_page.is_first_layer_group_route();
1101
1102 if is_root_directory || is_first_layer_group_route {
1105 check_and_update_module_references(
1106 app_dir.clone(),
1107 &mut modules.not_found,
1108 &mut parent_modules.not_found,
1109 "dist/client/components/builtin/not-found.js",
1110 is_first_layer_group_route,
1111 )
1112 .await?;
1113
1114 check_and_update_module_references(
1115 app_dir.clone(),
1116 &mut modules.forbidden,
1117 &mut parent_modules.forbidden,
1118 "dist/client/components/builtin/forbidden.js",
1119 is_first_layer_group_route,
1120 )
1121 .await?;
1122
1123 check_and_update_module_references(
1124 app_dir.clone(),
1125 &mut modules.unauthorized,
1126 &mut parent_modules.unauthorized,
1127 "dist/client/components/builtin/unauthorized.js",
1128 is_first_layer_group_route,
1129 )
1130 .await?;
1131 }
1132
1133 if is_root_directory {
1134 check_and_update_global_module_references(
1135 app_dir.clone(),
1136 &mut modules.global_error,
1137 "dist/client/components/builtin/global-error.js",
1138 )
1139 .await?;
1140 }
1141
1142 let mut tree = AppPageLoaderTree {
1143 page: app_page.clone(),
1144 segment: directory_name.clone(),
1145 parallel_routes: FxIndexMap::default(),
1146 modules: modules.without_leaves(),
1147 global_metadata: global_metadata.to_resolved().await?,
1148 };
1149
1150 let current_level_is_parallel_route = is_parallel_route(&directory_name);
1151
1152 if current_level_is_parallel_route {
1153 tree.segment = rcstr!("(slot)");
1154 }
1155
1156 if let Some(page) = (app_path == for_app_path || app_path.is_catchall())
1157 .then_some(modules.page)
1158 .flatten()
1159 {
1160 tree.parallel_routes.insert(
1161 rcstr!("children"),
1162 AppPageLoaderTree {
1163 page: app_page.clone(),
1164 segment: rcstr!("__PAGE__"),
1165 parallel_routes: FxIndexMap::default(),
1166 modules: AppDirModules {
1167 page: Some(page),
1168 metadata: modules.metadata,
1169 ..Default::default()
1170 },
1171 global_metadata: global_metadata.to_resolved().await?,
1172 },
1173 );
1174 }
1175
1176 let mut duplicate = FxHashMap::default();
1177
1178 for (subdir_name, subdirectory) in &directory_tree.subdirectories {
1179 let parallel_route_key = match_parallel_route(subdir_name);
1180
1181 let mut child_app_page = app_page.clone();
1182 let mut illegal_path_error = None;
1183
1184 if let Err(e) = child_app_page.push_str(&normalize_underscore(subdir_name)) {
1188 illegal_path_error = Some(e);
1189 }
1190
1191 let subtree = Box::pin(directory_tree_to_loader_tree_internal(
1192 app_dir.clone(),
1193 global_metadata,
1194 subdir_name.clone(),
1195 subdirectory,
1196 child_app_page.clone(),
1197 for_app_path.clone(),
1198 parent_modules.clone(),
1199 ))
1200 .await?;
1201
1202 if let Some(illegal_path) = subtree.as_ref().and(illegal_path_error) {
1203 return Err(illegal_path);
1204 }
1205
1206 if let Some(subtree) = subtree {
1207 if let Some(key) = parallel_route_key {
1208 let is_inside_catchall = app_page.is_catchall();
1219
1220 let is_leaf_segment = !has_child_routes(directory_tree);
1228
1229 let slot_has_children = has_child_routes(subdirectory);
1240
1241 if key != "children"
1242 && subdirectory.modules.default.is_none()
1243 && !is_inside_catchall
1244 && !is_leaf_segment
1245 && !slot_has_children
1246 {
1247 missing_default_parallel_route_issue(
1248 app_dir.clone(),
1249 app_page.clone(),
1250 key.into(),
1251 )
1252 .to_resolved()
1253 .await?
1254 .emit();
1255 }
1256
1257 tree.parallel_routes.insert(key.into(), subtree);
1258 continue;
1259 }
1260
1261 if is_group_route(subdir_name) && !subtree.has_page() {
1263 continue;
1264 }
1265
1266 if subtree.has_page() {
1267 check_duplicate(&mut duplicate, &subtree, app_dir.clone()).await?;
1268 }
1269
1270 if let Some(current_tree) = tree.parallel_routes.get("children") {
1271 if current_tree.has_only_catchall()
1272 && (!subtree.has_only_catchall()
1273 || current_tree.get_specificity() < subtree.get_specificity())
1274 {
1275 tree.parallel_routes
1276 .insert(rcstr!("children"), subtree.clone());
1277 }
1278 } else {
1279 tree.parallel_routes.insert(rcstr!("children"), subtree);
1280 }
1281 } else if let Some(key) = parallel_route_key {
1282 bail!(
1283 "missing page or default for parallel route `{}` (page: {})",
1284 key,
1285 app_page
1286 );
1287 }
1288 }
1289
1290 if tree
1293 .parallel_routes
1294 .iter()
1295 .any(|(_, parallel_tree)| parallel_tree.is_intercepting())
1296 {
1297 let mut keys_to_replace = Vec::new();
1298
1299 for (key, parallel_tree) in &tree.parallel_routes {
1300 if !parallel_tree.is_intercepting() {
1301 keys_to_replace.push(key.clone());
1302 }
1303 }
1304
1305 for key in keys_to_replace {
1306 let subdir_name: RcStr = format!("@{key}").into();
1307
1308 let default = if key == "children" {
1309 modules.default.clone()
1310 } else if let Some(subdirectory) = directory_tree.subdirectories.get(&subdir_name) {
1311 subdirectory.modules.default.clone()
1312 } else {
1313 None
1314 };
1315
1316 let is_inside_catchall = app_page.is_catchall();
1317
1318 let is_leaf_segment = !has_child_routes(directory_tree);
1320
1321 if default.is_none() && key != "children" && !is_inside_catchall && !is_leaf_segment {
1326 missing_default_parallel_route_issue(
1327 app_dir.clone(),
1328 app_page.clone(),
1329 key.clone(),
1330 )
1331 .to_resolved()
1332 .await?
1333 .emit();
1334 }
1335
1336 tree.parallel_routes.insert(
1337 key.clone(),
1338 default_route_tree(
1339 app_dir.clone(),
1340 global_metadata,
1341 app_page.clone(),
1342 default,
1343 key.clone(),
1344 for_app_path.clone(),
1345 )
1346 .await?,
1347 );
1348 }
1349 }
1350
1351 if tree.parallel_routes.is_empty() {
1352 if modules.default.is_some() || current_level_is_parallel_route {
1353 tree = default_route_tree(
1354 app_dir.clone(),
1355 global_metadata,
1356 app_page.clone(),
1357 modules.default.clone(),
1358 rcstr!("children"),
1359 for_app_path.clone(),
1360 )
1361 .await?;
1362 } else {
1363 return Ok(None);
1364 }
1365 } else if tree.parallel_routes.get("children").is_none() {
1366 tree.parallel_routes.insert(
1367 rcstr!("children"),
1368 default_route_tree(
1369 app_dir.clone(),
1370 global_metadata,
1371 app_page.clone(),
1372 modules.default.clone(),
1373 rcstr!("children"),
1374 for_app_path.clone(),
1375 )
1376 .await?,
1377 );
1378 }
1379
1380 if tree.parallel_routes.len() > 1
1381 && tree.parallel_routes.keys().next().map(|s| s.as_str()) != Some("children")
1382 {
1383 tree.parallel_routes
1385 .move_index(tree.parallel_routes.len() - 1, 0);
1386 }
1387
1388 Ok(Some(tree))
1389}
1390
1391async fn default_route_tree(
1392 app_dir: FileSystemPath,
1393 global_metadata: Vc<GlobalMetadata>,
1394 app_page: AppPage,
1395 default_component: Option<FileSystemPath>,
1396 slot_name: RcStr,
1397 for_app_path: AppPath,
1398) -> Result<AppPageLoaderTree> {
1399 Ok(AppPageLoaderTree {
1400 page: app_page.clone(),
1401 segment: rcstr!("__DEFAULT__"),
1402 parallel_routes: FxIndexMap::default(),
1403 modules: if let Some(default) = default_component {
1404 AppDirModules {
1405 default: Some(default),
1406 ..Default::default()
1407 }
1408 } else {
1409 let contains_interception = for_app_path.contains_interception();
1410
1411 let default_file = if contains_interception && slot_name == "children" {
1412 "dist/client/components/builtin/default-null.js"
1413 } else {
1414 "dist/client/components/builtin/default.js"
1415 };
1416
1417 AppDirModules {
1418 default: Some(get_next_package(app_dir).await?.join(default_file)?),
1419 ..Default::default()
1420 }
1421 },
1422 global_metadata: global_metadata.to_resolved().await?,
1423 })
1424}
1425
1426#[turbo_tasks::function]
1427async fn directory_tree_to_entrypoints_internal(
1428 app_dir: FileSystemPath,
1429 global_metadata: ResolvedVc<GlobalMetadata>,
1430 is_global_not_found_enabled: Vc<bool>,
1431 next_mode: Vc<NextMode>,
1432 directory_name: RcStr,
1433 directory_tree: Vc<DirectoryTree>,
1434 app_page: AppPage,
1435 root_layouts: ResolvedVc<FileSystemPathVec>,
1436 root_params: ResolvedVc<RootParamVecOption>,
1437) -> Result<Vc<Entrypoints>> {
1438 let span = tracing::info_span!("build layout trees", name = display(&app_page));
1439 directory_tree_to_entrypoints_internal_untraced(
1440 app_dir,
1441 global_metadata,
1442 is_global_not_found_enabled,
1443 next_mode,
1444 directory_name,
1445 directory_tree,
1446 app_page,
1447 root_layouts,
1448 root_params,
1449 )
1450 .instrument(span)
1451 .await
1452}
1453
1454async fn directory_tree_to_entrypoints_internal_untraced(
1455 app_dir: FileSystemPath,
1456 global_metadata: ResolvedVc<GlobalMetadata>,
1457 is_global_not_found_enabled: Vc<bool>,
1458 next_mode: Vc<NextMode>,
1459 directory_name: RcStr,
1460 directory_tree: Vc<DirectoryTree>,
1461 app_page: AppPage,
1462 root_layouts: ResolvedVc<FileSystemPathVec>,
1463 root_params: ResolvedVc<RootParamVecOption>,
1464) -> Result<Vc<Entrypoints>> {
1465 let mut result = FxIndexMap::default();
1466
1467 let directory_tree_vc = directory_tree;
1468 let directory_tree = &*directory_tree.await?;
1469
1470 let subdirectories = &directory_tree.subdirectories;
1471 let modules = &directory_tree.modules;
1472 let root_layouts = if let Some(layout) = &modules.layout {
1476 let mut layouts = root_layouts.owned().await?;
1477 layouts.push(layout.clone());
1478 ResolvedVc::cell(layouts)
1479 } else {
1480 root_layouts
1481 };
1482
1483 let root_params = if root_params.await?.is_none() && (*root_layouts.await?).len() == 1 {
1485 ResolvedVc::cell(Some(
1488 app_page
1489 .0
1490 .iter()
1491 .filter_map(|segment| match segment {
1492 PageSegment::Dynamic(param)
1493 | PageSegment::CatchAll(param)
1494 | PageSegment::OptionalCatchAll(param) => Some(param.clone()),
1495 _ => None,
1496 })
1497 .collect::<Vec<RcStr>>(),
1498 ))
1499 } else {
1500 root_params
1501 };
1502
1503 if modules.page.is_some() {
1504 let app_path = AppPath::from(app_page.clone());
1505
1506 let loader_tree = *directory_tree_to_loader_tree(
1507 app_dir.clone(),
1508 *global_metadata,
1509 directory_name.clone(),
1510 directory_tree_vc,
1511 app_page.clone(),
1512 app_path,
1513 )
1514 .await?;
1515
1516 add_app_page(
1517 app_dir.clone(),
1518 &mut result,
1519 app_page.complete(PageType::Page)?,
1520 loader_tree.context("loader tree should be created for a page/default")?,
1521 root_params,
1522 );
1523 }
1524
1525 if let Some(route) = &modules.route {
1526 add_app_route(
1527 app_dir.clone(),
1528 &mut result,
1529 app_page.complete(PageType::Route)?,
1530 route.clone(),
1531 root_layouts,
1532 root_params,
1533 );
1534 }
1535
1536 let Metadata {
1537 icon,
1538 apple,
1539 twitter,
1540 open_graph,
1541 sitemap,
1542 base_page: _,
1543 } = &modules.metadata;
1544
1545 for meta in sitemap
1546 .iter()
1547 .cloned()
1548 .chain(icon.iter().cloned().map(MetadataItem::from))
1549 .chain(apple.iter().cloned().map(MetadataItem::from))
1550 .chain(twitter.iter().cloned().map(MetadataItem::from))
1551 .chain(open_graph.iter().cloned().map(MetadataItem::from))
1552 {
1553 let app_page = app_page.clone_push_str(&get_metadata_route_name(meta.clone()).await?)?;
1554
1555 add_app_metadata_route(
1556 app_dir.clone(),
1557 &mut result,
1558 normalize_metadata_route(app_page)?,
1559 meta,
1560 root_params,
1561 );
1562 }
1563
1564 if app_page.is_root() {
1566 let GlobalMetadata {
1567 favicon,
1568 robots,
1569 manifest,
1570 } = &*global_metadata.await?;
1571
1572 for meta in favicon.iter().chain(robots.iter()).chain(manifest.iter()) {
1573 let app_page =
1574 app_page.clone_push_str(&get_metadata_route_name(meta.clone()).await?)?;
1575
1576 add_app_metadata_route(
1577 app_dir.clone(),
1578 &mut result,
1579 normalize_metadata_route(app_page)?,
1580 meta.clone(),
1581 root_params,
1582 );
1583 }
1584
1585 let mut modules = directory_tree.modules.clone();
1586
1587 if modules.layout.is_none() {
1589 modules.layout = Some(
1590 get_next_package(app_dir.clone())
1591 .await?
1592 .join("dist/client/components/builtin/layout.js")?,
1593 );
1594 }
1595
1596 if modules.not_found.is_none() {
1597 modules.not_found = Some(
1598 get_next_package(app_dir.clone())
1599 .await?
1600 .join("dist/client/components/builtin/not-found.js")?,
1601 );
1602 }
1603 if modules.forbidden.is_none() {
1604 modules.forbidden = Some(
1605 get_next_package(app_dir.clone())
1606 .await?
1607 .join("dist/client/components/builtin/forbidden.js")?,
1608 );
1609 }
1610 if modules.unauthorized.is_none() {
1611 modules.unauthorized = Some(
1612 get_next_package(app_dir.clone())
1613 .await?
1614 .join("dist/client/components/builtin/unauthorized.js")?,
1615 );
1616 }
1617
1618 let is_global_not_found_enabled = *is_global_not_found_enabled.await?;
1623 let use_global_not_found =
1624 is_global_not_found_enabled || modules.global_not_found.is_some();
1625
1626 let not_found_root_modules = modules.without_leaves();
1627 let not_found_tree = AppPageLoaderTree {
1628 page: app_page.clone(),
1629 segment: directory_name.clone(),
1630 parallel_routes: fxindexmap! {
1631 rcstr!("children") => AppPageLoaderTree {
1632 page: app_page.clone(),
1633 segment: rcstr!("/_not-found"),
1634 parallel_routes: fxindexmap! {
1635 rcstr!("children") => AppPageLoaderTree {
1636 page: app_page.clone(),
1637 segment: rcstr!("__PAGE__"),
1638 parallel_routes: FxIndexMap::default(),
1639 modules: if use_global_not_found {
1640 AppDirModules {
1643 page: Some(get_next_package(app_dir.clone())
1645 .await?
1646 .join("dist/client/components/builtin/empty-stub.js")?,
1647 ),
1648 ..Default::default()
1649 }
1650 } else {
1651 AppDirModules {
1654 page: match modules.not_found {
1655 Some(v) => Some(v),
1656 None => Some(get_next_package(app_dir.clone())
1657 .await?
1658 .join("dist/client/components/builtin/not-found.js")?,
1659 ),
1660 },
1661 ..Default::default()
1662 }
1663 },
1664 global_metadata,
1665 }
1666 },
1667 modules: AppDirModules {
1668 ..Default::default()
1669 },
1670 global_metadata,
1671 },
1672 },
1673 modules: AppDirModules {
1674 layout: if use_global_not_found {
1679 match modules.global_not_found {
1680 Some(v) => Some(v),
1681 None => Some(
1682 get_next_package(app_dir.clone())
1683 .await?
1684 .join("dist/client/components/builtin/global-not-found.js")?,
1685 ),
1686 }
1687 } else {
1688 modules.layout
1689 },
1690 ..not_found_root_modules
1691 },
1692 global_metadata,
1693 }
1694 .resolved_cell();
1695
1696 {
1697 let app_page = app_page
1698 .clone_push_str("_not-found")?
1699 .complete(PageType::Page)?;
1700
1701 add_app_page(
1702 app_dir.clone(),
1703 &mut result,
1704 app_page,
1705 not_found_tree,
1706 root_params,
1707 );
1708 }
1709
1710 if matches!(*next_mode.await?, NextMode::Build) {
1715 let global_error_tree = AppPageLoaderTree {
1717 page: app_page.clone(),
1718 segment: directory_name.clone(),
1719 parallel_routes: fxindexmap! {
1720 rcstr!("children") => AppPageLoaderTree {
1721 page: app_page.clone(),
1722 segment: rcstr!("__PAGE__"),
1723 parallel_routes: FxIndexMap::default(),
1724 modules: AppDirModules {
1725 page: Some(get_next_package(app_dir.clone())
1726 .await?
1727 .join("dist/client/components/builtin/app-error.js")?),
1728 ..Default::default()
1729 },
1730 global_metadata,
1731 }
1732 },
1733 modules: AppDirModules::default(),
1734 global_metadata,
1735 }
1736 .resolved_cell();
1737
1738 let app_global_error_page = app_page
1739 .clone_push_str("_global-error")?
1740 .complete(PageType::Page)?;
1741 add_app_page(
1742 app_dir.clone(),
1743 &mut result,
1744 app_global_error_page,
1745 global_error_tree,
1746 root_params,
1747 );
1748 }
1749 }
1750
1751 let app_page = &app_page;
1752 let directory_name = &directory_name;
1753 let subdirectories = subdirectories
1754 .iter()
1755 .map(|(subdir_name, &subdirectory)| {
1756 let app_dir = app_dir.clone();
1757
1758 async move {
1759 let mut child_app_page = app_page.clone();
1760 let mut illegal_path = None;
1761
1762 if let Err(e) = child_app_page.push_str(&normalize_underscore(subdir_name)) {
1766 illegal_path = Some(e);
1767 }
1768
1769 let map = directory_tree_to_entrypoints_internal(
1770 app_dir.clone(),
1771 *global_metadata,
1772 is_global_not_found_enabled,
1773 next_mode,
1774 subdir_name.clone(),
1775 *subdirectory,
1776 child_app_page.clone(),
1777 *root_layouts,
1778 *root_params,
1779 )
1780 .await?;
1781
1782 if let Some(illegal_path) = illegal_path
1783 && !map.is_empty()
1784 {
1785 return Err(illegal_path);
1786 }
1787
1788 let mut loader_trees = Vec::new();
1789
1790 for (_, entrypoint) in map.iter() {
1791 if let Entrypoint::AppPage { ref pages, .. } = *entrypoint {
1792 for page in pages {
1793 let app_path = AppPath::from(page.clone());
1794
1795 let loader_tree = directory_tree_to_loader_tree(
1796 app_dir.clone(),
1797 *global_metadata,
1798 directory_name.clone(),
1799 directory_tree_vc,
1800 app_page.clone(),
1801 app_path,
1802 );
1803 loader_trees.push(loader_tree);
1804 }
1805 }
1806 }
1807 Ok((map, loader_trees))
1808 }
1809 })
1810 .try_join()
1811 .await?;
1812
1813 for (map, loader_trees) in subdirectories.iter() {
1814 let mut i = 0;
1815 for (_, entrypoint) in map.iter() {
1816 match entrypoint {
1817 Entrypoint::AppPage {
1818 pages,
1819 loader_tree: _,
1820 root_params,
1821 } => {
1822 for page in pages {
1823 let loader_tree = *loader_trees[i].await?;
1824 i += 1;
1825
1826 add_app_page(
1827 app_dir.clone(),
1828 &mut result,
1829 page.clone(),
1830 loader_tree
1831 .context("loader tree should be created for a page/default")?,
1832 *root_params,
1833 );
1834 }
1835 }
1836 Entrypoint::AppRoute {
1837 page,
1838 path,
1839 root_layouts,
1840 root_params,
1841 } => {
1842 add_app_route(
1843 app_dir.clone(),
1844 &mut result,
1845 page.clone(),
1846 path.clone(),
1847 *root_layouts,
1848 *root_params,
1849 );
1850 }
1851 Entrypoint::AppMetadata {
1852 page,
1853 metadata,
1854 root_params,
1855 } => {
1856 add_app_metadata_route(
1857 app_dir.clone(),
1858 &mut result,
1859 page.clone(),
1860 metadata.clone(),
1861 *root_params,
1862 );
1863 }
1864 }
1865 }
1866 }
1867 Ok(Vc::cell(result))
1868}
1869
1870#[turbo_tasks::function]
1872pub async fn get_global_metadata(
1873 app_dir: FileSystemPath,
1874 page_extensions: Vc<Vec<RcStr>>,
1875) -> Result<Vc<GlobalMetadata>> {
1876 let DirectoryContent::Entries(entries) = &*app_dir.read_dir().await? else {
1877 bail!("app_dir must be a directory")
1878 };
1879 let mut metadata = GlobalMetadata::default();
1880
1881 for (basename, entry) in entries {
1882 let DirectoryEntry::File(file) = entry else {
1883 continue;
1884 };
1885
1886 let Some(GlobalMetadataFileMatch {
1887 metadata_type,
1888 dynamic,
1889 }) = match_global_metadata_file(basename, &page_extensions.await?)
1890 else {
1891 continue;
1892 };
1893
1894 let entry = match metadata_type {
1895 "favicon" => &mut metadata.favicon,
1896 "manifest" => &mut metadata.manifest,
1897 "robots" => &mut metadata.robots,
1898 _ => continue,
1899 };
1900
1901 if dynamic {
1902 *entry = Some(MetadataItem::Dynamic { path: file.clone() });
1903 } else {
1904 *entry = Some(MetadataItem::Static { path: file.clone() });
1905 }
1906 }
1908
1909 Ok(metadata.cell())
1910}
1911
1912#[turbo_tasks::value(shared)]
1913struct DirectoryTreeIssue {
1914 pub severity: IssueSeverity,
1915 pub app_dir: FileSystemPath,
1916 pub message: ResolvedVc<StyledString>,
1917}
1918
1919#[turbo_tasks::value_impl]
1920impl Issue for DirectoryTreeIssue {
1921 fn severity(&self) -> IssueSeverity {
1922 self.severity
1923 }
1924
1925 #[turbo_tasks::function]
1926 fn title(&self) -> Vc<StyledString> {
1927 StyledString::Text(rcstr!("An issue occurred while preparing your Next.js app")).cell()
1928 }
1929
1930 #[turbo_tasks::function]
1931 fn stage(&self) -> Vc<IssueStage> {
1932 IssueStage::AppStructure.cell()
1933 }
1934
1935 #[turbo_tasks::function]
1936 fn file_path(&self) -> Vc<FileSystemPath> {
1937 self.app_dir.clone().cell()
1938 }
1939
1940 #[turbo_tasks::function]
1941 fn description(&self) -> Vc<OptionStyledString> {
1942 Vc::cell(Some(self.message))
1943 }
1944}