next_core/
app_structure.rs

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