next_core/
app_structure.rs

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
30// Next.js ignores underscores for routes but you can use %5f to still serve an underscored
31// route.
32fn normalize_underscore(string: &str) -> String {
33    string.replace("%5F", "_")
34}
35
36/// A final route in the app directory.
37#[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/// A single metadata file plus an optional "alt" text file.
89#[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/// A single metadata file.
101#[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/// Metadata file that can be placed in any segment of the app directory.
148#[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    // The page indicates where the metadata is defined and captured.
163    // The steps for capturing metadata (get_directory_tree) and constructing
164    // LoaderTree (directory_tree_to_entrypoints) is separated,
165    // and child loader tree can trickle down metadata when clone / merge components calculates
166    // the actual path incorrectly with fillMetadataSegment.
167    //
168    // This is only being used for the static metadata files.
169    #[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/// Metadata files that can be placed in the root of the app directory.
192#[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    /// key is e.g. "dashboard", "(dashboard)", "@slot"
218    pub subdirectories: BTreeMap<RcStr, ResolvedVc<DirectoryTree>>,
219    pub modules: AppDirModules,
220}
221
222#[turbo_tasks::value]
223#[derive(Clone, Debug)]
224struct PlainDirectoryTree {
225    /// key is e.g. "dashboard", "(dashboard)", "@slot"
226    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/// Finds and returns the [DirectoryTree] of the app directory if existing.
252#[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        // the file watcher might invalidate things in the wrong order,
287        // and we have to account for the eventual consistency of turbo-tasks
288        // so we just return an empty tree here.
289        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                // Do not process .d.ts files as routes
310                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                // appDir ignores paths starting with an underscore
381                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            // TODO(WEB-952) handle symlinks in app dir
389            _ => {}
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    /// Returns true if there's a page match in this loader tree.
422    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    /// Returns whether the only match in this tree is for a catch-all
437    /// route.
438    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    /// Returns true if this loader tree contains an intercepting route match.
453    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    /// Returns the specificity of the page (i.e. the number of segments
468    /// affecting the path)
469    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            // loader trees should always match for the same path as they are generated by a
633            // turbo tasks function
634            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
952/// Checks if a directory tree has child routes (non-parallel, non-group routes).
953/// Leaf segments don't need default.js because there are no child routes
954/// that could cause the parallel slot to unmatch.
955fn has_child_routes(directory_tree: &PlainDirectoryTree) -> bool {
956    for (name, subdirectory) in &directory_tree.subdirectories {
957        // Skip parallel routes (start with '@')
958        if is_parallel_route(name) {
959            continue;
960        }
961
962        // Skip route groups, but check if they have pages inside
963        if is_group_route(name) {
964            // Recursively check if the group has child routes
965            if has_child_routes(subdirectory) {
966                return true;
967            }
968            continue;
969        }
970
971        // If we get here, it's a regular route segment (child route)
972        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/// creates the loader tree for a specific route (pathname / [AppPath])
1005#[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    // the page this loader tree is constructed for
1013    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
1031/// Checks the current module if it needs to be updated with the default page.
1032/// If the module is already set, update the parent module to the same value.
1033/// If the parent module is set and module is not set, set the module to the parent module.
1034/// If the module and the parent module are not set, set them to the default value.
1035///
1036/// # Arguments
1037/// * `app_dir` - The application directory.
1038/// * `module` - The current module to check and update if it is not set.
1039/// * `parent_module` - The parent module to update if the current module is set or both are not
1040///   set.
1041/// * `file_path` - The file path to the default page if neither the current module nor the parent
1042///   module is set.
1043/// * `is_first_layer_group_route` - If true, the module will be overridden with the parent module
1044///   if it is not set.
1045async 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        // If the module is set, update the parent module to the same value
1054        (Some(module), _) => *parent_module = Some(module.clone()),
1055        // If we are in a first layer group route and we have a parent module, we want to override
1056        // a nonexistent module with the parent module
1057        (None, Some(parent_module)) if is_first_layer_group_route => {
1058            *module = Some(parent_module.clone())
1059        }
1060        // If we are not in a first layer group route, and the module is not set, and the parent
1061        // module is set, we do nothing
1062        (None, Some(_)) => {}
1063        // If the module is not set, and the parent module is not set, we override with the default
1064        // page. This can only happen in the root directory because after this the parent module
1065        // will always be set.
1066        (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
1076/// Checks if the current directory is the root directory and if the module is not set.
1077/// If the module is not set, it will be set to the default page.
1078///
1079/// # Arguments
1080/// * `app_dir` - The application directory.
1081/// * `module` - The module to check and update if it is not set.
1082/// * `file_path` - The file path to the default page if the module is not set.
1083async 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    // the page this loader tree is constructed for
1102    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    // Capture the current page for the metadata to calculate segment relative to
1114    // the corresponding page for the static metadata files.
1115    modules.metadata.base_page = Some(app_page.clone());
1116
1117    // the root directory in the app dir.
1118    let is_root_directory = app_page.is_root();
1119
1120    // If the first layer is a group route, we treat it as root layer
1121    let is_first_layer_group_route = app_page.is_first_layer_group_route();
1122
1123    // Handle the non-global modules that should always be overridden for top level groups or set to
1124    // the default page if they are not set.
1125    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        // When constructing the app_page fails (e. g. due to limitations of the order),
1206        // we only want to emit the error when there are actual pages below that
1207        // directory.
1208        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                // Validate that parallel routes (except "children") have a default.js file.
1230                // This validation matches the webpack loader's logic but is implemented
1231                // differently due to Turbopack's single-pass recursive processing.
1232
1233                // Check if we're inside a catch-all route (i.e., the parallel route is a child
1234                // of a catch-all segment). Only skip validation if the slot is UNDER a catch-all.
1235                // For example:
1236                //   /[...catchAll]/@slot - is_inside_catchall = true (skip validation) ✓
1237                //   /@slot/[...catchAll] - is_inside_catchall = false (require default) ✓
1238                // The catch-all provides fallback behavior, so default.js is not required.
1239                let is_inside_catchall = app_page.is_catchall();
1240
1241                // Check if this is a leaf segment (no child routes).
1242                // Leaf segments don't need default.js because there are no child routes
1243                // that could cause the parallel slot to unmatch. For example:
1244                //   /repo-overview/@slot/page with no child routes - is_leaf_segment = true (skip
1245                // validation) ✓   /repo-overview/@slot/page with
1246                // /repo-overview/child/page - is_leaf_segment = false (require default) ✓
1247                // This also handles route groups correctly by filtering them out.
1248                let is_leaf_segment = !has_child_routes(directory_tree);
1249
1250                // Turbopack-specific: Check if the parallel slot has matching child routes.
1251                // In webpack, this is checked implicitly via the two-phase processing:
1252                // slots with content are processed first and skip validation in the second phase.
1253                // In Turbopack's single-pass approach, we check directly if the slot has child
1254                // routes. If the slot has child routes that match the parent's
1255                // child routes, it can render content for those routes and doesn't
1256                // need a default. For example:
1257                //   /parent/@slot/page + /parent/@slot/child + /parent/child - slot_has_children =
1258                // true (skip validation) ✓   /parent/@slot/page + /parent/child (no
1259                // @slot/child) - slot_has_children = false (require default) ✓
1260                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            // skip groups which don't have a page match.
1283            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    // make sure we don't have a match for other slots if there's an intercepting route match
1312    // we only check subtrees as the current level could trigger `is_intercepting`
1313    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            // Check if this is a leaf segment (no child routes).
1340            let is_leaf_segment = !has_child_routes(directory_tree);
1341
1342            // Only emit the issue if this is not the children slot and there's no default
1343            // component. The children slot is implicit and doesn't require a default.js
1344            // file. Also skip validation if the slot is UNDER a catch-all route or if
1345            // this is a leaf segment (no child routes).
1346            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        // children must go first for next.js to work correctly
1405        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    // Route can have its own segment config, also can inherit from the layout root
1494    // segment config. https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes#segment-runtime-option
1495    // Pass down layouts from each tree to apply segment config when adding route.
1496    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    // TODO: `root_layouts` is a misnomer, they're just parent layouts
1505    let root_params = if root_params.await?.is_none() && (*root_layouts.await?).len() == 1 {
1506        // found a root layout. the params up-to-and-including this point are the root params
1507        // for all child segments
1508        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    // root path: /
1586    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        // fill in the default modules for the not-found entrypoint
1609        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        // Next.js has this logic in "collect-app-paths", where the root not-found page
1640        // is considered as its own entry point.
1641
1642        // Determine if we enable the global not-found feature.
1643        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                                // if global-not-found.js is present:
1662                                // leaf module only keeps page pointing to empty-stub
1663                                AppDirModules {
1664                                    // page is built-in/empty-stub
1665                                    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                                // if global-not-found.js is not present:
1673                                // we search if we can compose root layout with the root not-found.js;
1674                                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                // `global-not-found.js` does not need a layout since it's included.
1696                // Skip it if it's present.
1697                // Otherwise, we need to compose it with the root layout to compose with
1698                // not-found.js boundary.
1699                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        // Create production global error page only in build mode
1732        // This aligns with webpack: default Pages entries (including /_error) are only added when
1733        // the build isn't app-only. If the build is app-only (no user pages/api), we should still
1734        // expose the app global error so runtime errors render, but we shouldn't emit it otherwise.
1735        if matches!(*next_mode.await?, NextMode::Build) {
1736            // Use built-in global-error.js to create a `_global-error/page` route.
1737            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                // When constructing the app_page fails (e. g. due to limitations of the order),
1784                // we only want to emit the error when there are actual pages below that
1785                // directory.
1786                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/// Returns the global metadata for an app directory.
1892#[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        // TODO(WEB-952) handle symlinks in app dir
1928    }
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}