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