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